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