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