[autotest] Factor mounting code into lxc_config.

Move the mount management into the DeployConfigManager, to reduce coupling
between it, the Container, and the ContainerBucket.

Add tests.

BUG=chromium:720219
TEST=sudo python lxc_config_unittest.py -v

Change-Id: I6abd539bab65760330cbdc99487557bd349052f4
Reviewed-on: https://chromium-review.googlesource.com/574974
Commit-Ready: Ben Kwa <[email protected]>
Tested-by: Ben Kwa <[email protected]>
Reviewed-by: Ilja H. Friedel <[email protected]>
diff --git a/site_utils/lxc/config.py b/site_utils/lxc/config.py
index e071273..dbff416 100644
--- a/site_utils/lxc/config.py
+++ b/site_utils/lxc/config.py
@@ -203,20 +203,26 @@
         return c
 
 
-    def __init__(self, container):
+    def __init__(self, container, config_file=None):
         """Initialize the deploy config manager.
 
         @param container: The container needs to deploy config.
-
+        @param config_file: An optional config file.  For testing.
         """
         self.container = container
         # If shadow config is used, the deployment procedure will skip some
         # special handling of config file, e.g.,
         # 1. Set enable_master_ssh to False in autotest shadow config.
         # 2. Set ssh logleve to ERROR for all hosts.
-        self.is_shadow_config = os.path.exists(SSP_DEPLOY_SHADOW_CONFIG_FILE)
-        config_file = (SSP_DEPLOY_SHADOW_CONFIG_FILE if self.is_shadow_config
-                       else SSP_DEPLOY_CONFIG_FILE)
+        if config_file is None:
+            self.is_shadow_config = os.path.exists(
+                    SSP_DEPLOY_SHADOW_CONFIG_FILE)
+            config_file = (
+                    SSP_DEPLOY_SHADOW_CONFIG_FILE if self.is_shadow_config
+                    else SSP_DEPLOY_CONFIG_FILE)
+        else:
+            self.is_shadow_config = False
+
         with open(config_file) as f:
             deploy_configs = json.load(f)
         self.deploy_configs = [self.validate(c) for c in deploy_configs
@@ -391,6 +397,9 @@
             if (mount_config.force_create and
                 not os.path.exists(mount_config.source)):
                 utils.run('mkdir -p %s' % mount_config.source)
+            self.container.mount_dir(mount_config.source,
+                                     mount_config.target,
+                                     mount_config.readonly)
 
 
     def deploy_post_start(self):
diff --git a/site_utils/lxc/container_bucket.py b/site_utils/lxc/container_bucket.py
index 1ea7e86..9dbcbc3 100644
--- a/site_utils/lxc/container_bucket.py
+++ b/site_utils/lxc/container_bucket.py
@@ -330,9 +330,6 @@
                           False),
                         ]
 
-        for mount_config in deploy_config_manager.mount_configs:
-            mount_entries.append((mount_config.source, mount_config.target,
-                                  mount_config.readonly))
         # Update container config to mount directories.
         for source, destination, readonly in mount_entries:
             container.mount_dir(source, destination, readonly)
diff --git a/site_utils/lxc/lxc_config_unittest.py b/site_utils/lxc/lxc_config_unittest.py
index 52184cb..02f11ef 100644
--- a/site_utils/lxc/lxc_config_unittest.py
+++ b/site_utils/lxc/lxc_config_unittest.py
@@ -3,13 +3,18 @@
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 
+import collections
 import json
 import os
+import shutil
+import tempfile
 import unittest
+from contextlib import contextmanager
 
 import common
+from autotest_lib.client.bin import utils
 from autotest_lib.site_utils.lxc import config as lxc_config
-
+from autotest_lib.site_utils.lxc import utils as lxc_utils
 
 class DeployConfigTest(unittest.TestCase):
     """Test DeployConfigManager.
@@ -26,5 +31,149 @@
             lxc_config.DeployConfigManager.validate(config)
 
 
-if '__main__':
+    def testPreStart(self):
+        """Verifies that pre-start works correctly.
+        Checks that mounts are correctly created in the container.
+        """
+        with TempDir() as tmpdir:
+            config = [
+                {
+                    'mount': True,
+                    'source': tempfile.mkdtemp(dir=tmpdir),
+                    'target': '/target0',
+                    'readonly': True,
+                    'force_create': False
+                },
+                {
+                    'mount': True,
+                    'source': tempfile.mkdtemp(dir=tmpdir),
+                    'target': '/target1',
+                    'readonly': False,
+                    'force_create': False
+                },
+            ]
+            with ConfigFile(config) as test_cfg, MockContainer() as container:
+                manager = lxc_config.DeployConfigManager(container, test_cfg)
+                manager.deploy_pre_start()
+                self.assertEqual(len(config), len(container.mounts))
+                for c in config:
+                    self.assertTrue(container.has_mount(c))
+
+
+    def testPreStartWithCreate(self):
+        """Verifies that pre-start creates mounted dirs.
+
+        Checks that missing mount points are created when force_create is
+        enabled.
+        """
+        with TempDir() as tmpdir:
+            src_dir = os.path.join(tmpdir, 'foobar')
+            config = [{
+                'mount': True,
+                'source': src_dir,
+                'target': '/target0',
+                'readonly': True,
+                'force_create': True
+            }]
+            with ConfigFile(config) as test_cfg, MockContainer() as container:
+                manager = lxc_config.DeployConfigManager(container, test_cfg)
+                # Pre-condition: the path doesn't exist.
+                self.assertFalse(lxc_utils.path_exists(src_dir))
+
+                # After calling deploy_pre_start, the path should exist and the
+                # mount should be created in the container.
+                manager.deploy_pre_start()
+                self.assertTrue(lxc_utils.path_exists(src_dir))
+                self.assertEqual(len(config), len(container.mounts))
+                for c in config:
+                    self.assertTrue(container.has_mount(c))
+
+
+class _MockContainer(object):
+    """A test mock for the container class.
+
+    Don't instantiate this directly, use the MockContainer context manager
+    defined below.
+    """
+
+    def __init__(self):
+        self.rootfs = tempfile.mkdtemp()
+        self.mounts = []
+        self.MountConfig = collections.namedtuple(
+                'MountConfig', ['source', 'destination', 'readonly'])
+
+
+    def cleanup(self):
+        """Clean up tmp dirs created by the container."""
+        # DeployConfigManager uses sudo to create some directories in the
+        # container, so it's necessary to use sudo to clean up.
+        utils.run('sudo rm -rf %s' % self.rootfs)
+
+
+    def mount_dir(self, src, dst, ro):
+        """Stub implementation of mount_dir.
+
+        Records calls for later verification.
+
+        @param src: Mount source dir.
+        @param dst: Mount destination dir.
+        @param ro: Read-only flag.
+        """
+        self.mounts.append(self.MountConfig(src, dst, ro))
+
+
+    def has_mount(self, config):
+        """Verifies whether an earlier call was made to mount_dir.
+
+        @param config: The config object to verify.
+
+        @return True if an earlier call was made to mount_dir that matches the
+                given mount configuration; False otherwise.
+        """
+        mount = self.MountConfig(config['source'],
+                                 config['target'],
+                                 config['readonly'])
+        return mount in self.mounts
+
+
+@contextmanager
+def MockContainer():
+    """Context manager for creating a _MockContainer for testing."""
+    container = _MockContainer()
+    try:
+        yield container
+    finally:
+        container.cleanup()
+
+
+@contextmanager
+def ConfigFile(config):
+    """Context manager for creating a config file.
+
+    The given configs are translated into json and pushed into a temporary file
+    that the DeployConfigManager can read.
+
+    @param config: A list of config objects.  Each config object is a dictionary
+                   which conforms to the format described in config.py.
+    """
+    with tempfile.NamedTemporaryFile() as tmp:
+        json.dump(config, tmp)
+        tmp.flush()
+        yield tmp.name
+
+
+@contextmanager
+def TempDir():
+    """Context manager for creating a temporary directory.
+
+    We have to mount something.  Make temporary directories to mount.
+    """
+    tmpdir = tempfile.mkdtemp()
+    try:
+        yield tmpdir
+    finally:
+        shutil.rmtree(tmpdir)
+
+
+if __name__ == '__main__':
     unittest.main()