1*9c5db199SXin Li#!/usr/bin/python3 2*9c5db199SXin Li# Copyright 2017 The Chromium OS Authors. All rights reserved. 3*9c5db199SXin Li# Use of this source code is governed by a BSD-style license that can be 4*9c5db199SXin Li# found in the LICENSE file. 5*9c5db199SXin Li 6*9c5db199SXin Liimport os 7*9c5db199SXin Liimport random 8*9c5db199SXin Liimport shutil 9*9c5db199SXin Liimport tempfile 10*9c5db199SXin Liimport unittest 11*9c5db199SXin Lifrom contextlib import contextmanager 12*9c5db199SXin Li 13*9c5db199SXin Liimport common 14*9c5db199SXin Lifrom autotest_lib.client.bin import utils 15*9c5db199SXin Lifrom autotest_lib.client.common_lib import error 16*9c5db199SXin Lifrom autotest_lib.site_utils import lxc 17*9c5db199SXin Lifrom autotest_lib.site_utils.lxc import base_image 18*9c5db199SXin Lifrom autotest_lib.site_utils.lxc import constants 19*9c5db199SXin Lifrom autotest_lib.site_utils.lxc import container as container_module 20*9c5db199SXin Lifrom autotest_lib.site_utils.lxc import unittest_http 21*9c5db199SXin Lifrom autotest_lib.site_utils.lxc import unittest_setup 22*9c5db199SXin Lifrom autotest_lib.site_utils.lxc import utils as lxc_utils 23*9c5db199SXin Li 24*9c5db199SXin Li 25*9c5db199SXin Liclass ContainerTests(lxc_utils.LXCTests): 26*9c5db199SXin Li """Unit tests for the Container class.""" 27*9c5db199SXin Li 28*9c5db199SXin Li @classmethod 29*9c5db199SXin Li def setUpClass(cls): 30*9c5db199SXin Li super(ContainerTests, cls).setUpClass() 31*9c5db199SXin Li cls.test_dir = tempfile.mkdtemp(dir=lxc.DEFAULT_CONTAINER_PATH, 32*9c5db199SXin Li prefix='container_unittest_') 33*9c5db199SXin Li 34*9c5db199SXin Li # Check if a base container exists on this machine and download one if 35*9c5db199SXin Li # necessary. 36*9c5db199SXin Li image = base_image.BaseImage(lxc.DEFAULT_CONTAINER_PATH, lxc.BASE) 37*9c5db199SXin Li try: 38*9c5db199SXin Li cls.base_container = image.get() 39*9c5db199SXin Li cls.cleanup_base_container = False 40*9c5db199SXin Li except error.ContainerError: 41*9c5db199SXin Li image.setup() 42*9c5db199SXin Li cls.base_container = image.get() 43*9c5db199SXin Li cls.cleanup_base_container = True 44*9c5db199SXin Li assert(cls.base_container is not None) 45*9c5db199SXin Li 46*9c5db199SXin Li @classmethod 47*9c5db199SXin Li def tearDownClass(cls): 48*9c5db199SXin Li cls.base_container = None 49*9c5db199SXin Li if not unittest_setup.config.skip_cleanup: 50*9c5db199SXin Li if cls.cleanup_base_container: 51*9c5db199SXin Li image = lxc.BaseImage(lxc.DEFAULT_CONTAINER_PATH, lxc.BASE) 52*9c5db199SXin Li image.cleanup() 53*9c5db199SXin Li utils.run('sudo rm -r %s' % cls.test_dir) 54*9c5db199SXin Li 55*9c5db199SXin Li def testInit(self): 56*9c5db199SXin Li """Verifies that containers initialize correctly.""" 57*9c5db199SXin Li # Make a container that just points to the base container. 58*9c5db199SXin Li container = lxc.Container.create_from_existing_dir( 59*9c5db199SXin Li self.base_container.container_path, 60*9c5db199SXin Li self.base_container.name) 61*9c5db199SXin Li # Calling is_running triggers an lxc-ls call, which should verify that 62*9c5db199SXin Li # the on-disk container is valid. 63*9c5db199SXin Li self.assertFalse(container.is_running()) 64*9c5db199SXin Li 65*9c5db199SXin Li def testInitInvalid(self): 66*9c5db199SXin Li """Verifies that invalid containers can still be instantiated, 67*9c5db199SXin Li if not used. 68*9c5db199SXin Li """ 69*9c5db199SXin Li with tempfile.NamedTemporaryFile(dir=self.test_dir) as tmpfile: 70*9c5db199SXin Li name = os.path.basename(tmpfile.name) 71*9c5db199SXin Li container = lxc.Container.create_from_existing_dir(self.test_dir, 72*9c5db199SXin Li name) 73*9c5db199SXin Li with self.assertRaises(error.ContainerError): 74*9c5db199SXin Li container.refresh_status() 75*9c5db199SXin Li 76*9c5db199SXin Li def testInvalidId(self): 77*9c5db199SXin Li """Verifies that corrupted ID files do not raise exceptions.""" 78*9c5db199SXin Li with self.createContainer() as container: 79*9c5db199SXin Li # Create a container with an empty ID file. 80*9c5db199SXin Li id_path = os.path.join(container.container_path, 81*9c5db199SXin Li container.name, 82*9c5db199SXin Li container_module._CONTAINER_ID_FILENAME) 83*9c5db199SXin Li utils.run('sudo touch %s' % id_path) 84*9c5db199SXin Li try: 85*9c5db199SXin Li # Verify that container creation doesn't raise exceptions. 86*9c5db199SXin Li test_container = lxc.Container.create_from_existing_dir( 87*9c5db199SXin Li self.test_dir, container.name) 88*9c5db199SXin Li self.assertIsNone(test_container.id) 89*9c5db199SXin Li except Exception: 90*9c5db199SXin Li self.fail('Unexpected exception:\n%s' % error.format_error()) 91*9c5db199SXin Li 92*9c5db199SXin Li def testDefaultHostname(self): 93*9c5db199SXin Li """Verifies that the zygote starts up with a default hostname that is 94*9c5db199SXin Li the lxc container name.""" 95*9c5db199SXin Li test_name = 'testHostname' 96*9c5db199SXin Li with self.createContainer(name=test_name) as container: 97*9c5db199SXin Li container.start(wait_for_network=True) 98*9c5db199SXin Li hostname = container.attach_run('hostname').stdout.strip() 99*9c5db199SXin Li self.assertEqual(test_name, hostname) 100*9c5db199SXin Li 101*9c5db199SXin Li def testSetHostnameRunning(self): 102*9c5db199SXin Li """Verifies that the hostname can be set on a running container.""" 103*9c5db199SXin Li with self.createContainer() as container: 104*9c5db199SXin Li expected_hostname = 'my-new-hostname' 105*9c5db199SXin Li container.start(wait_for_network=True) 106*9c5db199SXin Li container.set_hostname(expected_hostname) 107*9c5db199SXin Li hostname = container.attach_run('hostname -f').stdout.strip() 108*9c5db199SXin Li self.assertEqual(expected_hostname, hostname) 109*9c5db199SXin Li 110*9c5db199SXin Li def testSetHostnameNotRunningRaisesException(self): 111*9c5db199SXin Li """Verifies that set_hostname on a stopped container raises an error. 112*9c5db199SXin Li 113*9c5db199SXin Li The lxc.utsname config setting is unreliable (it only works if the 114*9c5db199SXin Li original container name is not a valid RFC-952 hostname, e.g. if it has 115*9c5db199SXin Li underscores). 116*9c5db199SXin Li 117*9c5db199SXin Li A more reliable method exists for setting the hostname but it requires 118*9c5db199SXin Li the container to be running. To avoid confusion, setting the hostname 119*9c5db199SXin Li on a stopped container is disallowed. 120*9c5db199SXin Li 121*9c5db199SXin Li This test verifies that the operation raises a ContainerError. 122*9c5db199SXin Li """ 123*9c5db199SXin Li with self.createContainer() as container: 124*9c5db199SXin Li with self.assertRaises(error.ContainerError): 125*9c5db199SXin Li # Ensure the container is not running 126*9c5db199SXin Li if container.is_running(): 127*9c5db199SXin Li raise RuntimeError('Container should not be running.') 128*9c5db199SXin Li container.set_hostname('foobar') 129*9c5db199SXin Li 130*9c5db199SXin Li def testClone(self): 131*9c5db199SXin Li """Verifies that cloning a container works as expected.""" 132*9c5db199SXin Li clone = lxc.Container.clone(src=self.base_container, 133*9c5db199SXin Li new_name="testClone", 134*9c5db199SXin Li new_path=self.test_dir, 135*9c5db199SXin Li snapshot=True) 136*9c5db199SXin Li try: 137*9c5db199SXin Li # Throws an exception if the container is not valid. 138*9c5db199SXin Li clone.refresh_status() 139*9c5db199SXin Li finally: 140*9c5db199SXin Li clone.destroy() 141*9c5db199SXin Li 142*9c5db199SXin Li def testCloneWithoutCleanup(self): 143*9c5db199SXin Li """Verifies that cloning a container to an existing name will fail as 144*9c5db199SXin Li expected. 145*9c5db199SXin Li """ 146*9c5db199SXin Li lxc.Container.clone(src=self.base_container, 147*9c5db199SXin Li new_name="testCloneWithoutCleanup", 148*9c5db199SXin Li new_path=self.test_dir, 149*9c5db199SXin Li snapshot=True) 150*9c5db199SXin Li with self.assertRaises(error.ContainerError): 151*9c5db199SXin Li lxc.Container.clone(src=self.base_container, 152*9c5db199SXin Li new_name="testCloneWithoutCleanup", 153*9c5db199SXin Li new_path=self.test_dir, 154*9c5db199SXin Li snapshot=True) 155*9c5db199SXin Li 156*9c5db199SXin Li def testCloneWithCleanup(self): 157*9c5db199SXin Li """Verifies that cloning a container with cleanup works properly.""" 158*9c5db199SXin Li clone0 = lxc.Container.clone(src=self.base_container, 159*9c5db199SXin Li new_name="testClone", 160*9c5db199SXin Li new_path=self.test_dir, 161*9c5db199SXin Li snapshot=True) 162*9c5db199SXin Li clone0.start(wait_for_network=False) 163*9c5db199SXin Li tmpfile = clone0.attach_run('mktemp').stdout 164*9c5db199SXin Li # Verify that our tmpfile exists 165*9c5db199SXin Li clone0.attach_run('test -f %s' % tmpfile) 166*9c5db199SXin Li 167*9c5db199SXin Li # Clone another container in place of the existing container. 168*9c5db199SXin Li clone1 = lxc.Container.clone(src=self.base_container, 169*9c5db199SXin Li new_name="testClone", 170*9c5db199SXin Li new_path=self.test_dir, 171*9c5db199SXin Li snapshot=True, 172*9c5db199SXin Li cleanup=True) 173*9c5db199SXin Li with self.assertRaises(error.CmdError): 174*9c5db199SXin Li clone1.attach_run('test -f %s' % tmpfile) 175*9c5db199SXin Li 176*9c5db199SXin Li def testInstallSsp(self): 177*9c5db199SXin Li """Verifies that installing the ssp in the container works.""" 178*9c5db199SXin Li # Hard-coded path to some golden data for this test. 179*9c5db199SXin Li test_ssp = os.path.join( 180*9c5db199SXin Li common.autotest_dir, 181*9c5db199SXin Li 'site_utils', 'lxc', 'test', 'test_ssp.tar.bz2') 182*9c5db199SXin Li # Create a container, install the self-served ssp, then check that it is 183*9c5db199SXin Li # installed into the container correctly. 184*9c5db199SXin Li with self.createContainer() as container: 185*9c5db199SXin Li with unittest_http.serve_locally(test_ssp) as url: 186*9c5db199SXin Li container.install_ssp(url) 187*9c5db199SXin Li container.start(wait_for_network=False) 188*9c5db199SXin Li 189*9c5db199SXin Li # The test ssp just contains a couple of text files, in known 190*9c5db199SXin Li # locations. Verify the location and content of those files in the 191*9c5db199SXin Li # container. 192*9c5db199SXin Li def cat(path): 193*9c5db199SXin Li """A helper method to run `cat`""" 194*9c5db199SXin Li return container.attach_run('cat %s' % path).stdout 195*9c5db199SXin Li 196*9c5db199SXin Li test0 = cat(os.path.join(constants.CONTAINER_AUTOTEST_DIR, 197*9c5db199SXin Li 'test.0')) 198*9c5db199SXin Li test1 = cat(os.path.join(constants.CONTAINER_AUTOTEST_DIR, 199*9c5db199SXin Li 'dir0', 'test.1')) 200*9c5db199SXin Li self.assertEquals('the five boxing wizards jumped quickly', 201*9c5db199SXin Li test0) 202*9c5db199SXin Li self.assertEquals('the quick brown fox jumps over the lazy dog', 203*9c5db199SXin Li test1) 204*9c5db199SXin Li 205*9c5db199SXin Li def testInstallControlFile(self): 206*9c5db199SXin Li """Verifies that installing a control file in the container works.""" 207*9c5db199SXin Li _unused, tmpfile = tempfile.mkstemp() 208*9c5db199SXin Li with self.createContainer() as container: 209*9c5db199SXin Li container.install_control_file(tmpfile) 210*9c5db199SXin Li container.start(wait_for_network=False) 211*9c5db199SXin Li # Verify that the file is found in the container. 212*9c5db199SXin Li container.attach_run( 213*9c5db199SXin Li 'test -f %s' % os.path.join(lxc.CONTROL_TEMP_PATH, 214*9c5db199SXin Li os.path.basename(tmpfile))) 215*9c5db199SXin Li 216*9c5db199SXin Li def testCopyFile(self): 217*9c5db199SXin Li """Verifies that files are correctly copied into the container.""" 218*9c5db199SXin Li control_string = 'amazingly few discotheques provide jukeboxes' 219*9c5db199SXin Li with tempfile.NamedTemporaryFile() as tmpfile: 220*9c5db199SXin Li tmpfile.write(control_string.encode('utf-8')) 221*9c5db199SXin Li tmpfile.flush() 222*9c5db199SXin Li 223*9c5db199SXin Li with self.createContainer() as container: 224*9c5db199SXin Li dst = os.path.join(constants.CONTAINER_AUTOTEST_DIR, 225*9c5db199SXin Li os.path.basename(tmpfile.name)) 226*9c5db199SXin Li container.copy(tmpfile.name, dst) 227*9c5db199SXin Li container.start(wait_for_network=False) 228*9c5db199SXin Li # Verify the file content. 229*9c5db199SXin Li test_string = container.attach_run('cat %s' % dst).stdout 230*9c5db199SXin Li self.assertEquals(control_string, test_string) 231*9c5db199SXin Li 232*9c5db199SXin Li def testCopyDirectory(self): 233*9c5db199SXin Li """Verifies that directories are correctly copied into the container.""" 234*9c5db199SXin Li control_string = 'pack my box with five dozen liquor jugs' 235*9c5db199SXin Li with lxc_utils.TempDir() as tmpdir: 236*9c5db199SXin Li fd, tmpfile = tempfile.mkstemp(dir=tmpdir) 237*9c5db199SXin Li f = os.fdopen(fd, 'w') 238*9c5db199SXin Li f.write(control_string) 239*9c5db199SXin Li f.close() 240*9c5db199SXin Li 241*9c5db199SXin Li with self.createContainer() as container: 242*9c5db199SXin Li dst = os.path.join(constants.CONTAINER_AUTOTEST_DIR, 243*9c5db199SXin Li os.path.basename(tmpdir)) 244*9c5db199SXin Li container.copy(tmpdir, dst) 245*9c5db199SXin Li container.start(wait_for_network=False) 246*9c5db199SXin Li # Verify the file content. 247*9c5db199SXin Li test_file = os.path.join(dst, os.path.basename(tmpfile)) 248*9c5db199SXin Li test_string = container.attach_run('cat %s' % test_file).stdout 249*9c5db199SXin Li self.assertEquals(control_string, test_string) 250*9c5db199SXin Li 251*9c5db199SXin Li def testMountDirectory(self): 252*9c5db199SXin Li """Verifies that read-write mounts work.""" 253*9c5db199SXin Li with lxc_utils.TempDir() as tmpdir, self.createContainer() as container: 254*9c5db199SXin Li dst = '/testMountDirectory/testMount' 255*9c5db199SXin Li container.mount_dir(tmpdir, dst, readonly=False) 256*9c5db199SXin Li container.start(wait_for_network=False) 257*9c5db199SXin Li 258*9c5db199SXin Li # Verify that the mount point is correctly bound, and is read-write. 259*9c5db199SXin Li self.verifyBindMount(container, dst, tmpdir) 260*9c5db199SXin Li container.attach_run('test -r %s -a -w %s' % (dst, dst)) 261*9c5db199SXin Li 262*9c5db199SXin Li def testMountDirectoryReadOnly(self): 263*9c5db199SXin Li """Verifies that read-only mounts work.""" 264*9c5db199SXin Li with lxc_utils.TempDir() as tmpdir, self.createContainer() as container: 265*9c5db199SXin Li dst = '/testMountDirectoryReadOnly/testMount' 266*9c5db199SXin Li container.mount_dir(tmpdir, dst, readonly=True) 267*9c5db199SXin Li container.start(wait_for_network=False) 268*9c5db199SXin Li 269*9c5db199SXin Li # Verify that the mount point is correctly bound, and is read-only. 270*9c5db199SXin Li self.verifyBindMount(container, dst, tmpdir) 271*9c5db199SXin Li container.attach_run('test -r %s -a ! -w %s' % (dst, dst)) 272*9c5db199SXin Li 273*9c5db199SXin Li def testMountDirectoryRelativePath(self): 274*9c5db199SXin Li """Verifies that relative-path mounts work.""" 275*9c5db199SXin Li with lxc_utils.TempDir() as tmpdir, self.createContainer() as container: 276*9c5db199SXin Li dst = 'testMountDirectoryRelativePath/testMount' 277*9c5db199SXin Li container.mount_dir(tmpdir, dst, readonly=True) 278*9c5db199SXin Li container.start(wait_for_network=False) 279*9c5db199SXin Li 280*9c5db199SXin Li # Verify that the mount points is correctly bound.. 281*9c5db199SXin Li self.verifyBindMount(container, dst, tmpdir) 282*9c5db199SXin Li 283*9c5db199SXin Li def testContainerIdPersistence(self): 284*9c5db199SXin Li """Verifies that container IDs correctly persist. 285*9c5db199SXin Li 286*9c5db199SXin Li When a Container is instantiated on top of an existing container dir, 287*9c5db199SXin Li check that it picks up the correct ID. 288*9c5db199SXin Li """ 289*9c5db199SXin Li with self.createContainer() as container: 290*9c5db199SXin Li test_id = random_container_id() 291*9c5db199SXin Li container.id = test_id 292*9c5db199SXin Li 293*9c5db199SXin Li # Set up another container and verify that its ID matches. 294*9c5db199SXin Li test_container = lxc.Container.create_from_existing_dir( 295*9c5db199SXin Li container.container_path, container.name) 296*9c5db199SXin Li 297*9c5db199SXin Li self.assertEqual(test_id, test_container.id) 298*9c5db199SXin Li 299*9c5db199SXin Li def testContainerIdIsNone_newContainer(self): 300*9c5db199SXin Li """Verifies that newly created/cloned containers have no ID.""" 301*9c5db199SXin Li with self.createContainer() as container: 302*9c5db199SXin Li self.assertIsNone(container.id) 303*9c5db199SXin Li # Set an ID, clone the container, and verify the clone has no ID. 304*9c5db199SXin Li container.id = random_container_id() 305*9c5db199SXin Li clone = lxc.Container.clone(src=container, 306*9c5db199SXin Li new_name=container.name + '_clone', 307*9c5db199SXin Li snapshot=True) 308*9c5db199SXin Li self.assertIsNotNone(container.id) 309*9c5db199SXin Li self.assertIsNone(clone.id) 310*9c5db199SXin Li 311*9c5db199SXin Li @contextmanager 312*9c5db199SXin Li def createContainer(self, name=None): 313*9c5db199SXin Li """Creates a container from the base container, for testing. 314*9c5db199SXin Li Use this to ensure that containers get properly cleaned up after each 315*9c5db199SXin Li test. 316*9c5db199SXin Li 317*9c5db199SXin Li @param name: An optional name for the new container. 318*9c5db199SXin Li """ 319*9c5db199SXin Li if name is None: 320*9c5db199SXin Li name = self.id().split('.')[-1] 321*9c5db199SXin Li container = lxc.Container.clone(src=self.base_container, 322*9c5db199SXin Li new_name=name, 323*9c5db199SXin Li new_path=self.test_dir, 324*9c5db199SXin Li snapshot=True) 325*9c5db199SXin Li try: 326*9c5db199SXin Li yield container 327*9c5db199SXin Li finally: 328*9c5db199SXin Li if not unittest_setup.config.skip_cleanup: 329*9c5db199SXin Li container.destroy() 330*9c5db199SXin Li 331*9c5db199SXin Li def verifyBindMount(self, container, container_path, host_path): 332*9c5db199SXin Li """Verifies that a given path in a container is bind-mounted to a given 333*9c5db199SXin Li path in the host system. 334*9c5db199SXin Li 335*9c5db199SXin Li @param container: The Container instance to be tested. 336*9c5db199SXin Li @param container_path: The path in the container to compare. 337*9c5db199SXin Li @param host_path: The path in the host system to compare. 338*9c5db199SXin Li """ 339*9c5db199SXin Li container_inode = (container.attach_run('ls -id %s' % container_path) 340*9c5db199SXin Li .stdout.split()[0]) 341*9c5db199SXin Li host_inode = utils.run('ls -id %s' % host_path).stdout.split()[0] 342*9c5db199SXin Li # Compare the container and host inodes - they should match. 343*9c5db199SXin Li self.assertEqual(container_inode, host_inode) 344*9c5db199SXin Li 345*9c5db199SXin Li 346*9c5db199SXin Liclass ContainerIdTests(lxc_utils.LXCTests): 347*9c5db199SXin Li """Unit tests for the ContainerId class.""" 348*9c5db199SXin Li 349*9c5db199SXin Li def setUp(self): 350*9c5db199SXin Li self.test_dir = tempfile.mkdtemp() 351*9c5db199SXin Li 352*9c5db199SXin Li def tearDown(self): 353*9c5db199SXin Li shutil.rmtree(self.test_dir) 354*9c5db199SXin Li 355*9c5db199SXin Li def testPickle(self): 356*9c5db199SXin Li """Verifies the ContainerId persistence code.""" 357*9c5db199SXin Li # Create a random ID, then save and load it and compare them. 358*9c5db199SXin Li control = random_container_id() 359*9c5db199SXin Li control.save(self.test_dir) 360*9c5db199SXin Li 361*9c5db199SXin Li test_data = lxc.ContainerId.load(self.test_dir) 362*9c5db199SXin Li self.assertEqual(control, test_data) 363*9c5db199SXin Li 364*9c5db199SXin Li 365*9c5db199SXin Lidef random_container_id(): 366*9c5db199SXin Li """Generate a random container ID for testing.""" 367*9c5db199SXin Li return lxc.ContainerId.create(random.randint(0, 1000)) 368*9c5db199SXin Li 369*9c5db199SXin Li 370*9c5db199SXin Liif __name__ == '__main__': 371*9c5db199SXin Li unittest.main() 372