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