Ben Kwa | c0ce545 | 2017-07-12 12:12:46 +0800 | [diff] [blame^] | 1 | #!/usr/bin/python |
| 2 | # Copyright 2017 The Chromium OS Authors. All rights reserved. |
| 3 | # Use of this source code is governed by a BSD-style license that can be |
| 4 | # found in the LICENSE file. |
| 5 | |
| 6 | import argparse |
| 7 | import logging |
| 8 | import os |
| 9 | import tempfile |
| 10 | import shutil |
| 11 | import sys |
| 12 | import unittest |
| 13 | from contextlib import contextmanager |
| 14 | |
| 15 | import common |
| 16 | from autotest_lib.client.bin import utils |
| 17 | from autotest_lib.site_utils import lxc |
| 18 | from autotest_lib.site_utils.lxc import config as lxc_config |
| 19 | from autotest_lib.site_utils.lxc import unittest_logging |
| 20 | from autotest_lib.site_utils.lxc import utils as lxc_utils |
| 21 | |
| 22 | |
| 23 | options = None |
| 24 | |
| 25 | class ZygoteTests(unittest.TestCase): |
| 26 | """Unit tests for the Zygote class.""" |
| 27 | |
| 28 | @classmethod |
| 29 | def setUpClass(cls): |
| 30 | cls.test_dir = tempfile.mkdtemp(dir=lxc.DEFAULT_CONTAINER_PATH, |
| 31 | prefix='zygote_unittest_') |
| 32 | cls.shared_host_path = os.path.join(cls.test_dir, 'host') |
| 33 | |
| 34 | # Use a container bucket just to download and set up the base image. |
| 35 | cls.bucket = lxc.ContainerBucket(cls.test_dir, cls.shared_host_path) |
| 36 | |
| 37 | if cls.bucket.base_container is None: |
| 38 | logging.debug('Base container not found - reinitializing') |
| 39 | cls.bucket.setup_base() |
| 40 | |
| 41 | cls.base_container = cls.bucket.base_container |
| 42 | assert(cls.base_container is not None) |
| 43 | |
| 44 | |
| 45 | @classmethod |
| 46 | def tearDownClass(cls): |
| 47 | cls.base_container = None |
| 48 | if not options.skip_cleanup: |
| 49 | cls.bucket.destroy_all() |
| 50 | shutil.rmtree(cls.test_dir) |
| 51 | |
| 52 | def tearDown(self): |
| 53 | # Ensure host dirs from each test are completely destroyed. |
| 54 | for host_dir in os.listdir(self.shared_host_path): |
| 55 | host_dir = os.path.realpath(os.path.join(self.shared_host_path, |
| 56 | host_dir)) |
| 57 | lxc_utils.cleanup_host_mount(host_dir); |
| 58 | |
| 59 | |
| 60 | def testCleanup(self): |
| 61 | """Verifies that the zygote cleans up after itself.""" |
| 62 | with self.createZygote() as zygote: |
| 63 | host_path = zygote.host_path |
| 64 | |
| 65 | self.assertTrue(os.path.isdir(host_path)) |
| 66 | |
| 67 | # Start/stop the zygote to exercise the host mounts. |
| 68 | zygote.start(wait_for_network=False) |
| 69 | zygote.stop() |
| 70 | |
| 71 | # After the zygote is destroyed, verify that the host path is cleaned |
| 72 | # up. |
| 73 | self.assertFalse(os.path.isdir(host_path)) |
| 74 | |
| 75 | |
| 76 | def testCleanupWithUnboundHostDir(self): |
| 77 | """Verifies that cleanup works when the host dir is unbound.""" |
| 78 | with self.createZygote() as zygote: |
| 79 | host_path = zygote.host_path |
| 80 | |
| 81 | self.assertTrue(os.path.isdir(host_path)) |
| 82 | # Don't start the zygote, so the host mount is not bound. |
| 83 | |
| 84 | # After the zygote is destroyed, verify that the host path is cleaned |
| 85 | # up. |
| 86 | self.assertFalse(os.path.isdir(host_path)) |
| 87 | |
| 88 | |
| 89 | def testCleanupWithNoHostDir(self): |
| 90 | """Verifies that cleanup works when the host dir is missing.""" |
| 91 | with self.createZygote() as zygote: |
| 92 | host_path = zygote.host_path |
| 93 | |
| 94 | utils.run('sudo rmdir %s' % zygote.host_path) |
| 95 | self.assertFalse(os.path.isdir(host_path)) |
| 96 | # Zygote destruction should yield no errors if the host path is |
| 97 | # missing. |
| 98 | |
| 99 | |
| 100 | def testHostname(self): |
| 101 | """Verifies that the zygote starts up with a default hostname that is |
| 102 | the lxc container name.""" |
| 103 | test_name = 'testHostname' |
| 104 | with self.createZygote(name=test_name) as zygote: |
| 105 | zygote.start(wait_for_network=True) |
| 106 | hostname = zygote.attach_run('hostname -f').stdout.strip() |
| 107 | self.assertEqual(test_name, hostname) |
| 108 | |
| 109 | |
| 110 | @unittest.skip('Setting the container hostname using lxc.utsname does not' |
| 111 | 'work on goobuntu.') |
| 112 | def testSetHostnameNotRunning(self): |
| 113 | """Verifies that the hostname can be set on a stopped container.""" |
| 114 | with self.createZygote() as zygote: |
| 115 | expected_hostname = 'my-new-hostname' |
| 116 | zygote.set_hostname(expected_hostname) |
| 117 | zygote.start(wait_for_network=True) |
| 118 | hostname = zygote.attach_run('hostname -f').stdout.strip() |
| 119 | self.assertEqual(expected_hostname, hostname) |
| 120 | |
| 121 | |
| 122 | def testSetHostnameRunning(self): |
| 123 | """Verifies that the hostname can be set on a running container.""" |
| 124 | with self.createZygote() as zygote: |
| 125 | expected_hostname = 'my-new-hostname' |
| 126 | zygote.start(wait_for_network=True) |
| 127 | zygote.set_hostname(expected_hostname) |
| 128 | hostname = zygote.attach_run('hostname -f').stdout.strip() |
| 129 | self.assertEqual(expected_hostname, hostname) |
| 130 | |
| 131 | |
| 132 | def testHostDir(self): |
| 133 | """Verifies that the host dir on the container is created, and correctly |
| 134 | bind-mounted.""" |
| 135 | with self.createZygote() as zygote: |
| 136 | self.assertIsNotNone(zygote.host_path) |
| 137 | self.assertTrue(os.path.isdir(zygote.host_path)) |
| 138 | |
| 139 | zygote.start(wait_for_network=False) |
| 140 | |
| 141 | self.verifyBindMount( |
| 142 | zygote, |
| 143 | container_path=lxc_config.CONTAINER_AUTOTEST_DIR, |
| 144 | host_path=zygote.host_path) |
| 145 | |
| 146 | |
| 147 | def testHostDirExists(self): |
| 148 | """Verifies that the host dir is just mounted if it already exists.""" |
| 149 | # Pre-create the host dir and put a file in it. |
| 150 | test_host_path = os.path.join(self.shared_host_path, |
| 151 | 'testHostDirExists') |
| 152 | test_filename = 'test_file' |
| 153 | test_host_file = os.path.join(test_host_path, test_filename) |
| 154 | test_string = 'jackdaws love my big sphinx of quartz.' |
| 155 | os.mkdir(test_host_path) |
| 156 | with open(test_host_file, 'w+') as f: |
| 157 | f.write(test_string) |
| 158 | |
| 159 | # Sanity check |
| 160 | self.assertTrue(lxc_utils.path_exists(test_host_file)) |
| 161 | |
| 162 | with self.createZygote(host_path=test_host_path) as zygote: |
| 163 | zygote.start(wait_for_network=False) |
| 164 | |
| 165 | self.verifyBindMount( |
| 166 | zygote, |
| 167 | container_path=lxc_config.CONTAINER_AUTOTEST_DIR, |
| 168 | host_path=zygote.host_path) |
| 169 | |
| 170 | # Verify that the old directory contents was preserved. |
| 171 | cmd = 'cat %s' % os.path.join(lxc_config.CONTAINER_AUTOTEST_DIR, |
| 172 | test_filename) |
| 173 | test_output = zygote.attach_run(cmd).stdout.strip() |
| 174 | self.assertEqual(test_string, test_output) |
| 175 | |
| 176 | |
| 177 | @contextmanager |
| 178 | def createZygote(self, |
| 179 | name = None, |
| 180 | attribute_values = None, |
| 181 | snapshot = True, |
| 182 | host_path = None): |
| 183 | """Clones a zygote from the test base container. |
| 184 | Use this to ensure that zygotes got properly cleaned up after each test. |
| 185 | |
| 186 | @param container_path: The LXC path for the new container. |
| 187 | @param host_path: The host path for the new container. |
| 188 | @param name: The name of the new container. |
| 189 | @param attribute_values: Any attribute values for the new container. |
| 190 | @param snapshot: Whether to create a snapshot clone. |
| 191 | """ |
| 192 | if name is None: |
| 193 | name = self.id().split('.')[-1] |
| 194 | if host_path is None: |
| 195 | host_path = os.path.join(self.shared_host_path, name) |
| 196 | if attribute_values is None: |
| 197 | attribute_values = {} |
| 198 | zygote = lxc.Zygote(self.test_dir, |
| 199 | name, |
| 200 | attribute_values, |
| 201 | self.base_container, |
| 202 | snapshot, |
| 203 | host_path) |
| 204 | yield zygote |
| 205 | if not options.skip_cleanup: |
| 206 | zygote.destroy() |
| 207 | |
| 208 | |
| 209 | def verifyBindMount(self, container, container_path, host_path): |
| 210 | """Verifies that a given path in a container is bind-mounted to a given |
| 211 | path in the host system. |
| 212 | |
| 213 | @param container: The Container instance to be tested. |
| 214 | @param container_path: The path in the container to compare. |
| 215 | @param host_path: The path in the host system to compare. |
| 216 | """ |
| 217 | container_inode = (container.attach_run('ls -id %s' % container_path) |
| 218 | .stdout.split()[0]) |
| 219 | host_inode = utils.run('ls -id %s' % host_path).stdout.split()[0] |
| 220 | # Compare the container and host inodes - they should match. |
| 221 | self.assertEqual(container_inode, host_inode) |
| 222 | |
| 223 | |
| 224 | def parse_options(): |
| 225 | """Parse command line inputs. |
| 226 | """ |
| 227 | parser = argparse.ArgumentParser() |
| 228 | parser.add_argument('-v', '--verbose', action='store_true', |
| 229 | help='Print out ALL entries.') |
| 230 | parser.add_argument('--skip_cleanup', action='store_true', |
| 231 | help='Skip deleting test containers.') |
| 232 | args, argv = parser.parse_known_args() |
| 233 | |
| 234 | # Hack: python unittest also processes args. Construct an argv to pass to |
| 235 | # it, that filters out the options it won't recognize. |
| 236 | if args.verbose: |
| 237 | argv.append('-v') |
| 238 | argv.insert(0, sys.argv[0]) |
| 239 | |
| 240 | return args, argv |
| 241 | |
| 242 | |
| 243 | if __name__ == '__main__': |
| 244 | options, unittest_argv = parse_options() |
| 245 | |
| 246 | |
| 247 | log_level=(logging.DEBUG if options.verbose else logging.INFO) |
| 248 | unittest_logging.setup(log_level) |
| 249 | |
| 250 | unittest.main(argv=unittest_argv) |