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 |
Ben Kwa | c0ce545 | 2017-07-12 12:12:46 +0800 | [diff] [blame] | 18 | from autotest_lib.site_utils.lxc import unittest_logging |
| 19 | from autotest_lib.site_utils.lxc import utils as lxc_utils |
| 20 | |
| 21 | |
| 22 | options = None |
| 23 | |
Ben Kwa | 36952eb | 2017-07-12 23:41:40 +0800 | [diff] [blame] | 24 | @unittest.skipIf(lxc.IS_MOBLAB, 'Zygotes are not supported on moblab.') |
Ben Kwa | c0ce545 | 2017-07-12 12:12:46 +0800 | [diff] [blame] | 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 | |
Ben Kwa | c0ce545 | 2017-07-12 12:12:46 +0800 | [diff] [blame] | 100 | def testSetHostnameRunning(self): |
| 101 | """Verifies that the hostname can be set on a running container.""" |
| 102 | with self.createZygote() as zygote: |
| 103 | expected_hostname = 'my-new-hostname' |
| 104 | zygote.start(wait_for_network=True) |
| 105 | zygote.set_hostname(expected_hostname) |
| 106 | hostname = zygote.attach_run('hostname -f').stdout.strip() |
| 107 | self.assertEqual(expected_hostname, hostname) |
| 108 | |
| 109 | |
| 110 | def testHostDir(self): |
| 111 | """Verifies that the host dir on the container is created, and correctly |
| 112 | bind-mounted.""" |
| 113 | with self.createZygote() as zygote: |
| 114 | self.assertIsNotNone(zygote.host_path) |
| 115 | self.assertTrue(os.path.isdir(zygote.host_path)) |
| 116 | |
| 117 | zygote.start(wait_for_network=False) |
| 118 | |
| 119 | self.verifyBindMount( |
| 120 | zygote, |
Ben Kwa | 36952eb | 2017-07-12 23:41:40 +0800 | [diff] [blame] | 121 | container_path=lxc.CONTAINER_AUTOTEST_DIR, |
Ben Kwa | c0ce545 | 2017-07-12 12:12:46 +0800 | [diff] [blame] | 122 | host_path=zygote.host_path) |
| 123 | |
| 124 | |
| 125 | def testHostDirExists(self): |
| 126 | """Verifies that the host dir is just mounted if it already exists.""" |
| 127 | # Pre-create the host dir and put a file in it. |
| 128 | test_host_path = os.path.join(self.shared_host_path, |
| 129 | 'testHostDirExists') |
| 130 | test_filename = 'test_file' |
| 131 | test_host_file = os.path.join(test_host_path, test_filename) |
| 132 | test_string = 'jackdaws love my big sphinx of quartz.' |
| 133 | os.mkdir(test_host_path) |
| 134 | with open(test_host_file, 'w+') as f: |
| 135 | f.write(test_string) |
| 136 | |
| 137 | # Sanity check |
| 138 | self.assertTrue(lxc_utils.path_exists(test_host_file)) |
| 139 | |
| 140 | with self.createZygote(host_path=test_host_path) as zygote: |
| 141 | zygote.start(wait_for_network=False) |
| 142 | |
| 143 | self.verifyBindMount( |
| 144 | zygote, |
Ben Kwa | 36952eb | 2017-07-12 23:41:40 +0800 | [diff] [blame] | 145 | container_path=lxc.CONTAINER_AUTOTEST_DIR, |
Ben Kwa | c0ce545 | 2017-07-12 12:12:46 +0800 | [diff] [blame] | 146 | host_path=zygote.host_path) |
| 147 | |
| 148 | # Verify that the old directory contents was preserved. |
Ben Kwa | 36952eb | 2017-07-12 23:41:40 +0800 | [diff] [blame] | 149 | cmd = 'cat %s' % os.path.join(lxc.CONTAINER_AUTOTEST_DIR, |
Ben Kwa | c0ce545 | 2017-07-12 12:12:46 +0800 | [diff] [blame] | 150 | test_filename) |
| 151 | test_output = zygote.attach_run(cmd).stdout.strip() |
| 152 | self.assertEqual(test_string, test_output) |
| 153 | |
| 154 | |
| 155 | @contextmanager |
| 156 | def createZygote(self, |
| 157 | name = None, |
| 158 | attribute_values = None, |
| 159 | snapshot = True, |
| 160 | host_path = None): |
| 161 | """Clones a zygote from the test base container. |
| 162 | Use this to ensure that zygotes got properly cleaned up after each test. |
| 163 | |
| 164 | @param container_path: The LXC path for the new container. |
| 165 | @param host_path: The host path for the new container. |
| 166 | @param name: The name of the new container. |
| 167 | @param attribute_values: Any attribute values for the new container. |
| 168 | @param snapshot: Whether to create a snapshot clone. |
| 169 | """ |
| 170 | if name is None: |
| 171 | name = self.id().split('.')[-1] |
| 172 | if host_path is None: |
| 173 | host_path = os.path.join(self.shared_host_path, name) |
| 174 | if attribute_values is None: |
| 175 | attribute_values = {} |
| 176 | zygote = lxc.Zygote(self.test_dir, |
| 177 | name, |
| 178 | attribute_values, |
| 179 | self.base_container, |
| 180 | snapshot, |
| 181 | host_path) |
Ben Kwa | 5a2cac1 | 2017-07-17 13:18:59 +0800 | [diff] [blame^] | 182 | try: |
| 183 | yield zygote |
| 184 | finally: |
| 185 | if not options.skip_cleanup: |
| 186 | zygote.destroy() |
Ben Kwa | c0ce545 | 2017-07-12 12:12:46 +0800 | [diff] [blame] | 187 | |
| 188 | |
| 189 | def verifyBindMount(self, container, container_path, host_path): |
| 190 | """Verifies that a given path in a container is bind-mounted to a given |
| 191 | path in the host system. |
| 192 | |
| 193 | @param container: The Container instance to be tested. |
| 194 | @param container_path: The path in the container to compare. |
| 195 | @param host_path: The path in the host system to compare. |
| 196 | """ |
| 197 | container_inode = (container.attach_run('ls -id %s' % container_path) |
| 198 | .stdout.split()[0]) |
| 199 | host_inode = utils.run('ls -id %s' % host_path).stdout.split()[0] |
| 200 | # Compare the container and host inodes - they should match. |
| 201 | self.assertEqual(container_inode, host_inode) |
| 202 | |
| 203 | |
| 204 | def parse_options(): |
| 205 | """Parse command line inputs. |
| 206 | """ |
| 207 | parser = argparse.ArgumentParser() |
| 208 | parser.add_argument('-v', '--verbose', action='store_true', |
| 209 | help='Print out ALL entries.') |
| 210 | parser.add_argument('--skip_cleanup', action='store_true', |
| 211 | help='Skip deleting test containers.') |
| 212 | args, argv = parser.parse_known_args() |
| 213 | |
| 214 | # Hack: python unittest also processes args. Construct an argv to pass to |
| 215 | # it, that filters out the options it won't recognize. |
| 216 | if args.verbose: |
| 217 | argv.append('-v') |
| 218 | argv.insert(0, sys.argv[0]) |
| 219 | |
| 220 | return args, argv |
| 221 | |
| 222 | |
| 223 | if __name__ == '__main__': |
| 224 | options, unittest_argv = parse_options() |
| 225 | |
Ben Kwa | c0ce545 | 2017-07-12 12:12:46 +0800 | [diff] [blame] | 226 | log_level=(logging.DEBUG if options.verbose else logging.INFO) |
| 227 | unittest_logging.setup(log_level) |
| 228 | |
| 229 | unittest.main(argv=unittest_argv) |