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