[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()