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 | c639523 | 2017-07-17 14:44:08 +0800 | [diff] [blame] | 18 | from autotest_lib.site_utils.lxc import constants |
| 19 | from autotest_lib.site_utils.lxc import unittest_http |
Ben Kwa | c0ce545 | 2017-07-12 12:12:46 +0800 | [diff] [blame] | 20 | from autotest_lib.site_utils.lxc import unittest_logging |
| 21 | from autotest_lib.site_utils.lxc import utils as lxc_utils |
Ben Kwa | a028259 | 2017-07-19 00:08:16 +0800 | [diff] [blame] | 22 | from autotest_lib.site_utils.lxc.unittest_container_bucket \ |
| 23 | import FastContainerBucket |
Ben Kwa | c0ce545 | 2017-07-12 12:12:46 +0800 | [diff] [blame] | 24 | |
| 25 | |
| 26 | options = None |
| 27 | |
Ben Kwa | 36952eb | 2017-07-12 23:41:40 +0800 | [diff] [blame] | 28 | @unittest.skipIf(lxc.IS_MOBLAB, 'Zygotes are not supported on moblab.') |
Ben Kwa | c0ce545 | 2017-07-12 12:12:46 +0800 | [diff] [blame] | 29 | class ZygoteTests(unittest.TestCase): |
| 30 | """Unit tests for the Zygote class.""" |
| 31 | |
| 32 | @classmethod |
| 33 | def setUpClass(cls): |
| 34 | cls.test_dir = tempfile.mkdtemp(dir=lxc.DEFAULT_CONTAINER_PATH, |
| 35 | prefix='zygote_unittest_') |
| 36 | cls.shared_host_path = os.path.join(cls.test_dir, 'host') |
| 37 | |
| 38 | # Use a container bucket just to download and set up the base image. |
Ben Kwa | a028259 | 2017-07-19 00:08:16 +0800 | [diff] [blame] | 39 | cls.bucket = FastContainerBucket(cls.test_dir, cls.shared_host_path) |
Ben Kwa | c0ce545 | 2017-07-12 12:12:46 +0800 | [diff] [blame] | 40 | |
| 41 | if cls.bucket.base_container is None: |
| 42 | logging.debug('Base container not found - reinitializing') |
| 43 | cls.bucket.setup_base() |
| 44 | |
| 45 | cls.base_container = cls.bucket.base_container |
| 46 | assert(cls.base_container is not None) |
| 47 | |
| 48 | |
| 49 | @classmethod |
| 50 | def tearDownClass(cls): |
| 51 | cls.base_container = None |
| 52 | if not options.skip_cleanup: |
| 53 | cls.bucket.destroy_all() |
| 54 | shutil.rmtree(cls.test_dir) |
| 55 | |
| 56 | def tearDown(self): |
| 57 | # Ensure host dirs from each test are completely destroyed. |
| 58 | for host_dir in os.listdir(self.shared_host_path): |
| 59 | host_dir = os.path.realpath(os.path.join(self.shared_host_path, |
| 60 | host_dir)) |
| 61 | lxc_utils.cleanup_host_mount(host_dir); |
| 62 | |
| 63 | |
| 64 | def testCleanup(self): |
| 65 | """Verifies that the zygote cleans up after itself.""" |
| 66 | with self.createZygote() as zygote: |
| 67 | host_path = zygote.host_path |
| 68 | |
| 69 | self.assertTrue(os.path.isdir(host_path)) |
| 70 | |
| 71 | # Start/stop the zygote to exercise the host mounts. |
| 72 | zygote.start(wait_for_network=False) |
| 73 | zygote.stop() |
| 74 | |
| 75 | # After the zygote is destroyed, verify that the host path is cleaned |
| 76 | # up. |
| 77 | self.assertFalse(os.path.isdir(host_path)) |
| 78 | |
| 79 | |
| 80 | def testCleanupWithUnboundHostDir(self): |
| 81 | """Verifies that cleanup works when the host dir is unbound.""" |
| 82 | with self.createZygote() as zygote: |
| 83 | host_path = zygote.host_path |
| 84 | |
| 85 | self.assertTrue(os.path.isdir(host_path)) |
| 86 | # Don't start the zygote, so the host mount is not bound. |
| 87 | |
| 88 | # After the zygote is destroyed, verify that the host path is cleaned |
| 89 | # up. |
| 90 | self.assertFalse(os.path.isdir(host_path)) |
| 91 | |
| 92 | |
| 93 | def testCleanupWithNoHostDir(self): |
| 94 | """Verifies that cleanup works when the host dir is missing.""" |
| 95 | with self.createZygote() as zygote: |
| 96 | host_path = zygote.host_path |
| 97 | |
| 98 | utils.run('sudo rmdir %s' % zygote.host_path) |
| 99 | self.assertFalse(os.path.isdir(host_path)) |
| 100 | # Zygote destruction should yield no errors if the host path is |
| 101 | # missing. |
| 102 | |
| 103 | |
Ben Kwa | c0ce545 | 2017-07-12 12:12:46 +0800 | [diff] [blame] | 104 | def testSetHostnameRunning(self): |
| 105 | """Verifies that the hostname can be set on a running container.""" |
| 106 | with self.createZygote() as zygote: |
| 107 | expected_hostname = 'my-new-hostname' |
| 108 | zygote.start(wait_for_network=True) |
| 109 | zygote.set_hostname(expected_hostname) |
| 110 | hostname = zygote.attach_run('hostname -f').stdout.strip() |
| 111 | self.assertEqual(expected_hostname, hostname) |
| 112 | |
| 113 | |
| 114 | def testHostDir(self): |
| 115 | """Verifies that the host dir on the container is created, and correctly |
| 116 | bind-mounted.""" |
| 117 | with self.createZygote() as zygote: |
| 118 | self.assertIsNotNone(zygote.host_path) |
| 119 | self.assertTrue(os.path.isdir(zygote.host_path)) |
| 120 | |
| 121 | zygote.start(wait_for_network=False) |
| 122 | |
| 123 | self.verifyBindMount( |
| 124 | zygote, |
Ben Kwa | 55293cd | 2017-07-26 22:26:42 +0800 | [diff] [blame] | 125 | container_path=lxc.CONTAINER_HOST_DIR, |
Ben Kwa | c0ce545 | 2017-07-12 12:12:46 +0800 | [diff] [blame] | 126 | host_path=zygote.host_path) |
| 127 | |
| 128 | |
| 129 | def testHostDirExists(self): |
| 130 | """Verifies that the host dir is just mounted if it already exists.""" |
| 131 | # Pre-create the host dir and put a file in it. |
| 132 | test_host_path = os.path.join(self.shared_host_path, |
| 133 | 'testHostDirExists') |
| 134 | test_filename = 'test_file' |
| 135 | test_host_file = os.path.join(test_host_path, test_filename) |
| 136 | test_string = 'jackdaws love my big sphinx of quartz.' |
| 137 | os.mkdir(test_host_path) |
| 138 | with open(test_host_file, 'w+') as f: |
| 139 | f.write(test_string) |
| 140 | |
| 141 | # Sanity check |
| 142 | self.assertTrue(lxc_utils.path_exists(test_host_file)) |
| 143 | |
| 144 | with self.createZygote(host_path=test_host_path) as zygote: |
| 145 | zygote.start(wait_for_network=False) |
| 146 | |
| 147 | self.verifyBindMount( |
| 148 | zygote, |
Ben Kwa | 55293cd | 2017-07-26 22:26:42 +0800 | [diff] [blame] | 149 | container_path=lxc.CONTAINER_HOST_DIR, |
Ben Kwa | c0ce545 | 2017-07-12 12:12:46 +0800 | [diff] [blame] | 150 | host_path=zygote.host_path) |
| 151 | |
| 152 | # Verify that the old directory contents was preserved. |
Ben Kwa | 55293cd | 2017-07-26 22:26:42 +0800 | [diff] [blame] | 153 | cmd = 'cat %s' % os.path.join(lxc.CONTAINER_HOST_DIR, |
Ben Kwa | c0ce545 | 2017-07-12 12:12:46 +0800 | [diff] [blame] | 154 | test_filename) |
| 155 | test_output = zygote.attach_run(cmd).stdout.strip() |
| 156 | self.assertEqual(test_string, test_output) |
| 157 | |
| 158 | |
Ben Kwa | c639523 | 2017-07-17 14:44:08 +0800 | [diff] [blame] | 159 | def testInstallSsp(self): |
| 160 | """Verifies that installing the ssp in the container works.""" |
| 161 | # Hard-coded path to some golden data for this test. |
| 162 | test_ssp = os.path.join( |
| 163 | common.autotest_dir, |
| 164 | 'site_utils', 'lxc', 'test', 'test_ssp.tar.bz2') |
| 165 | # Create a container, install the self-served ssp, then check that it is |
| 166 | # installed into the container correctly. |
| 167 | with self.createZygote() as zygote: |
| 168 | # Note: start the zygote first, then install the SSP. This mimics |
| 169 | # the way things would work in the production environment. |
| 170 | zygote.start(wait_for_network=False) |
| 171 | with unittest_http.serve_locally(test_ssp) as url: |
| 172 | zygote.install_ssp(url) |
| 173 | |
| 174 | # The test ssp just contains a couple of text files, in known |
| 175 | # locations. Verify the location and content of those files in the |
| 176 | # container. |
| 177 | cat = lambda path: zygote.attach_run('cat %s' % path).stdout |
| 178 | test0 = cat(os.path.join(constants.CONTAINER_AUTOTEST_DIR, |
| 179 | 'test.0')) |
| 180 | test1 = cat(os.path.join(constants.CONTAINER_AUTOTEST_DIR, |
| 181 | 'dir0', 'test.1')) |
| 182 | self.assertEquals('the five boxing wizards jumped quickly', |
| 183 | test0) |
| 184 | self.assertEquals('the quick brown fox jumps over the lazy dog', |
| 185 | test1) |
| 186 | |
| 187 | |
| 188 | def testInstallControlFile(self): |
| 189 | """Verifies that installing a control file in the container works.""" |
| 190 | _unused, tmpfile = tempfile.mkstemp() |
| 191 | with self.createZygote() as zygote: |
| 192 | # Note: start the zygote first. This mimics the way things would |
| 193 | # work in the production environment. |
| 194 | zygote.start(wait_for_network=False) |
| 195 | zygote.install_control_file(tmpfile) |
| 196 | # Verify that the file is found in the zygote. |
| 197 | zygote.attach_run( |
| 198 | 'test -f %s' % os.path.join(lxc.CONTROL_TEMP_PATH, |
| 199 | os.path.basename(tmpfile))) |
| 200 | |
| 201 | |
Ben Kwa | 55293cd | 2017-07-26 22:26:42 +0800 | [diff] [blame] | 202 | def testCopyFile(self): |
| 203 | """Verifies that files are correctly copied into the container.""" |
| 204 | control_string = 'amazingly few discotheques provide jukeboxes' |
| 205 | with tempfile.NamedTemporaryFile() as tmpfile: |
| 206 | tmpfile.write(control_string) |
| 207 | tmpfile.flush() |
| 208 | |
| 209 | with self.createZygote() as zygote: |
| 210 | dst = os.path.join(constants.CONTAINER_AUTOTEST_DIR, |
| 211 | os.path.basename(tmpfile.name)) |
| 212 | zygote.start(wait_for_network=False) |
| 213 | zygote.copy(tmpfile.name, dst) |
| 214 | # Verify the file content. |
| 215 | test_string = zygote.attach_run('cat %s' % dst).stdout |
| 216 | self.assertEquals(control_string, test_string) |
| 217 | |
| 218 | |
| 219 | def testCopyDirectory(self): |
| 220 | """Verifies that directories are correctly copied into the container.""" |
| 221 | control_string = 'pack my box with five dozen liquor jugs' |
| 222 | with lxc_utils.TempDir() as tmpdir: |
| 223 | fd, tmpfile = tempfile.mkstemp(dir=tmpdir) |
| 224 | f = os.fdopen(fd, 'w') |
| 225 | f.write(control_string) |
| 226 | f.close() |
| 227 | |
| 228 | with self.createZygote() as zygote: |
| 229 | dst = os.path.join(constants.CONTAINER_AUTOTEST_DIR, |
| 230 | os.path.basename(tmpdir)) |
| 231 | zygote.start(wait_for_network=False) |
| 232 | zygote.copy(tmpdir, dst) |
| 233 | # Verify the file content. |
| 234 | test_file = os.path.join(dst, os.path.basename(tmpfile)) |
| 235 | test_string = zygote.attach_run('cat %s' % test_file).stdout |
| 236 | self.assertEquals(control_string, test_string) |
| 237 | |
| 238 | |
Ben Kwa | c0ce545 | 2017-07-12 12:12:46 +0800 | [diff] [blame] | 239 | @contextmanager |
| 240 | def createZygote(self, |
| 241 | name = None, |
| 242 | attribute_values = None, |
| 243 | snapshot = True, |
| 244 | host_path = None): |
| 245 | """Clones a zygote from the test base container. |
| 246 | Use this to ensure that zygotes got properly cleaned up after each test. |
| 247 | |
| 248 | @param container_path: The LXC path for the new container. |
| 249 | @param host_path: The host path for the new container. |
| 250 | @param name: The name of the new container. |
| 251 | @param attribute_values: Any attribute values for the new container. |
| 252 | @param snapshot: Whether to create a snapshot clone. |
| 253 | """ |
| 254 | if name is None: |
| 255 | name = self.id().split('.')[-1] |
| 256 | if host_path is None: |
| 257 | host_path = os.path.join(self.shared_host_path, name) |
| 258 | if attribute_values is None: |
| 259 | attribute_values = {} |
| 260 | zygote = lxc.Zygote(self.test_dir, |
| 261 | name, |
| 262 | attribute_values, |
| 263 | self.base_container, |
| 264 | snapshot, |
| 265 | host_path) |
Ben Kwa | 5a2cac1 | 2017-07-17 13:18:59 +0800 | [diff] [blame] | 266 | try: |
| 267 | yield zygote |
| 268 | finally: |
| 269 | if not options.skip_cleanup: |
| 270 | zygote.destroy() |
Ben Kwa | c0ce545 | 2017-07-12 12:12:46 +0800 | [diff] [blame] | 271 | |
| 272 | |
| 273 | def verifyBindMount(self, container, container_path, host_path): |
| 274 | """Verifies that a given path in a container is bind-mounted to a given |
| 275 | path in the host system. |
| 276 | |
| 277 | @param container: The Container instance to be tested. |
| 278 | @param container_path: The path in the container to compare. |
| 279 | @param host_path: The path in the host system to compare. |
| 280 | """ |
| 281 | container_inode = (container.attach_run('ls -id %s' % container_path) |
| 282 | .stdout.split()[0]) |
| 283 | host_inode = utils.run('ls -id %s' % host_path).stdout.split()[0] |
| 284 | # Compare the container and host inodes - they should match. |
| 285 | self.assertEqual(container_inode, host_inode) |
| 286 | |
| 287 | |
| 288 | def parse_options(): |
| 289 | """Parse command line inputs. |
| 290 | """ |
| 291 | parser = argparse.ArgumentParser() |
| 292 | parser.add_argument('-v', '--verbose', action='store_true', |
| 293 | help='Print out ALL entries.') |
| 294 | parser.add_argument('--skip_cleanup', action='store_true', |
| 295 | help='Skip deleting test containers.') |
| 296 | args, argv = parser.parse_known_args() |
| 297 | |
| 298 | # Hack: python unittest also processes args. Construct an argv to pass to |
| 299 | # it, that filters out the options it won't recognize. |
| 300 | if args.verbose: |
Ben Kwa | c639523 | 2017-07-17 14:44:08 +0800 | [diff] [blame] | 301 | argv.insert(0, '-v') |
Ben Kwa | c0ce545 | 2017-07-12 12:12:46 +0800 | [diff] [blame] | 302 | argv.insert(0, sys.argv[0]) |
| 303 | |
| 304 | return args, argv |
| 305 | |
| 306 | |
| 307 | if __name__ == '__main__': |
| 308 | options, unittest_argv = parse_options() |
| 309 | |
Ben Kwa | c0ce545 | 2017-07-12 12:12:46 +0800 | [diff] [blame] | 310 | log_level=(logging.DEBUG if options.verbose else logging.INFO) |
| 311 | unittest_logging.setup(log_level) |
| 312 | |
| 313 | unittest.main(argv=unittest_argv) |