xref: /aosp_15_r20/external/autotest/site_utils/lxc/container_unittest.py (revision 9c5db1993ded3edbeafc8092d69fe5de2ee02df7)
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