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