test_docker.py 15.7 KB
Newer Older
André Anjos's avatar
André Anjos committed
1 2 3
#!/usr/bin/env python
# vim: set fileencoding=utf-8 :

4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
###################################################################################
#                                                                                 #
# Copyright (c) 2019 Idiap Research Institute, http://www.idiap.ch/               #
# Contact: beat.support@idiap.ch                                                  #
#                                                                                 #
# Redistribution and use in source and binary forms, with or without              #
# modification, are permitted provided that the following conditions are met:     #
#                                                                                 #
# 1. Redistributions of source code must retain the above copyright notice, this  #
# list of conditions and the following disclaimer.                                #
#                                                                                 #
# 2. Redistributions in binary form must reproduce the above copyright notice,    #
# this list of conditions and the following disclaimer in the documentation       #
# and/or other materials provided with the distribution.                          #
#                                                                                 #
# 3. Neither the name of the copyright holder nor the names of its contributors   #
# may be used to endorse or promote products derived from this software without   #
# specific prior written permission.                                              #
#                                                                                 #
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND #
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED   #
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE          #
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE    #
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL      #
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR      #
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER      #
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,   #
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE   #
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.            #
#                                                                                 #
###################################################################################


"""Asynchronous process I/O with the Subprocess module"""
André Anjos's avatar
André Anjos committed
38 39 40

import os
import time
Samuel GAIST's avatar
Samuel GAIST committed
41
import tempfile
42
import unittest
André Anjos's avatar
André Anjos committed
43 44
import pkg_resources

45 46
from tempfile import TemporaryDirectory

47
from ..dock import Host
48

49
from . import tmp_prefix
50
from .utils import slow
51 52
from .utils import skipif

53
from . import network_name
54
from . import DOCKER_NETWORK_TEST_ENABLED
55

56

57 58 59 60
class NoDiscoveryTests(unittest.TestCase):
    """Test cases that don't require the discovery of database and runtime
    environments.
    """
61

Philip ABBET's avatar
Philip ABBET committed
62 63
    @classmethod
    def setUpClass(cls):
64
        cls.host = Host(raise_on_errors=False, discover=False)
65

Philip ABBET's avatar
Philip ABBET committed
66 67 68
    @classmethod
    def tearDownClass(cls):
        cls.host.teardown()
69

Philip ABBET's avatar
Philip ABBET committed
70 71
    def tearDown(self):
        self.host.teardown()
Samuel GAIST's avatar
Samuel GAIST committed
72
        self.assertFalse(self.host.containers)  # All containers are gone
73 74


75 76
class NetworkTest(NoDiscoveryTests):
    @slow
77
    @skipif(not DOCKER_NETWORK_TEST_ENABLED, "Network test disabled")
78 79
    def test_network(self):
        string = "hello world"
80
        container = self.host.create_container("debian:8.4", ["echo", string])
81 82 83 84 85
        container.network_name = network_name

        try:
            self.host.start(container)
            status = self.host.wait(container)
Samuel GAIST's avatar
Samuel GAIST committed
86 87 88
        except Exception:
            from . import network

89 90 91 92
            network.remove()
            raise

        self.assertEqual(status, 0)
93
        self.assertEqual(self.host.logs(container), string + "\n")
94 95

    @slow
96
    @skipif(not DOCKER_NETWORK_TEST_ENABLED, "Network test disabled")
97 98 99
    def test_non_existing_network(self):

        string = "hello world"
100 101
        network_name = "beat.core.fake"
        container = self.host.create_container("debian:8.4", ["echo", string])
102 103 104 105 106
        container.network_name = network_name

        try:
            self.host.start(container)
        except RuntimeError as e:
107
            self.assertTrue(str(e).find("network %s not found" % network_name) >= 0)
108 109 110 111 112 113 114 115


class UserTest(NoDiscoveryTests):
    @slow
    def test_user(self):
        """Test that the uid property is correctly used.
        """

116
        container = self.host.create_container("debian:8.4", ["id"])
117 118 119 120 121 122
        container.uid = 10000

        self.host.start(container)
        status = self.host.wait(container)

        self.assertEqual(status, 0)
123 124 125 126 127
        self.assertTrue(
            self.host.logs(container).startswith(
                "uid={0} gid={0}".format(container.uid)
            )
        )
128 129


130 131 132 133 134 135
class EnvironmentVariableTest(NoDiscoveryTests):
    @slow
    def test_environment_variable(self):
        """Test that the uid property is correctly used.
        """

136 137
        container = self.host.create_container("debian:8.4", ["env"])
        container.add_environment_variable("DOCKER_TEST", "good")
138 139 140 141 142

        self.host.start(container)
        status = self.host.wait(container)

        self.assertEqual(status, 0)
143
        self.assertTrue("DOCKER_TEST=good" in self.host.logs(container))
144 145


146 147 148 149 150 151 152 153
class WorkdirTest(NoDiscoveryTests):
    @slow
    def test_workdir(self):
        """Test that the workdir property is correctly used.
        """

        with TemporaryDirectory() as tmp_folder:
            test_file = "test.txt"
154 155 156 157 158
            container = self.host.create_container(
                "debian:8.4", ["cp", "/etc/debian_version", test_file]
            )
            container.add_volume(tmp_folder, "/test_workdir", read_only=False)
            container.set_workdir("/test_workdir")
159 160 161 162 163 164 165 166 167 168 169 170 171

            self.host.start(container)
            status = self.host.wait(container)
            if status != 0:
                print(self.host.logs(container))

            self.assertEqual(status, 0)

            with open(os.path.join(tmp_folder, test_file), "rt") as file:
                content = file.read()
                self.assertEqual(content, "8.4\n")


172 173 174 175 176 177
class EntrypointTest(NoDiscoveryTests):
    @slow
    def test_entrypoint(self):
        """Test that the entrypoint property is correctly used.
        """

178 179
        container = self.host.create_container("debian:8.4", ["42"])
        container.set_entrypoint("echo")
180 181 182 183 184 185 186 187 188 189 190

        self.host.start(container)
        status = self.host.wait(container)
        logs = self.host.logs(container)
        if status != 0:
            print(logs)

        self.assertEqual(status, 0)
        self.assertEqual(logs, "42\n")


191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246
class TmpfsTest(NoDiscoveryTests):
    def test_tmpfs(self):
        """Test that the tmpfs are properly mounted and usable.
        """

        container = self.host.create_container(
            "debian:8.4", ["touch", "/dummy/test.txt"]
        )

        tmpfs_list = container.temporary_filesystems

        self.assertEqual(len(tmpfs_list), 2)

        container.add_tmpfs("/dummy", "1M")

        tmpfs_list = container.temporary_filesystems

        self.assertEqual(len(tmpfs_list), 3)

        self.host.start(container)
        status = self.host.wait(container)
        logs = self.host.logs(container)
        if status != 0:
            print(logs)

        self.assertEqual(status, 0)
        self.assertEqual(logs, "")

    def test_tmpfs_size(self):
        """Test that the tmpfs are respected.
        """

        container = self.host.create_container(
            "debian:8.4", ["dd", "if=/dev/zero", "of=/dummy/test.txt"]
        )

        tmpfs_list = container.temporary_filesystems

        self.assertEqual(len(tmpfs_list), 2)

        container.add_tmpfs("/dummy", "1M")

        tmpfs_list = container.temporary_filesystems

        self.assertEqual(len(tmpfs_list), 3)

        self.host.start(container)
        status = self.host.wait(container)
        logs = self.host.logs(container)
        if status != 0:
            print(logs)

        self.assertEqual(status, 1)
        self.assertTrue("No space left" in logs)


247
class AsyncTest(NoDiscoveryTests):
Philip ABBET's avatar
Philip ABBET committed
248 249
    @slow
    def test_echo(self):
250

Philip ABBET's avatar
Philip ABBET committed
251
        string = "hello, world"
252

253
        container = self.host.create_container("debian:8.4", ["echo", string])
Philip ABBET's avatar
Philip ABBET committed
254 255
        self.host.start(container)
        status = self.host.wait(container)
256

Philip ABBET's avatar
Philip ABBET committed
257
        self.assertEqual(status, 0)
258
        self.assertEqual(self.host.logs(container), string + "\n")
259

Philip ABBET's avatar
Philip ABBET committed
260 261
    @slow
    def test_non_existing(self):
262

263
        container = self.host.create_container("debian:8.4", ["sdfsdfdsf329909092"])
264

265 266
        try:
            self.host.start(container)
267
        except Exception as e:
268
            self.assertTrue(str(e).find("Failed to create the container") >= 0)
269

270
        self.assertFalse(self.host.containers)  # All containers are gone
271

Philip ABBET's avatar
Philip ABBET committed
272 273
    @slow
    def test_timeout(self):
274

275
        sleep_for = 100  # seconds
276

277
        container = self.host.create_container("debian:8.4", ["sleep", str(sleep_for)])
Philip ABBET's avatar
Philip ABBET committed
278
        self.host.start(container)
279

280 281
        retval = self.host.wait(container, timeout=0.5)
        self.assertTrue(retval is None)
282

Philip ABBET's avatar
Philip ABBET committed
283
        self.host.kill(container)
284

285
        retval = self.host.wait(container)
286

287
        self.assertEqual(self.host.status(container), "exited")
288
        self.assertEqual(retval, 137)
289
        self.assertEqual(self.host.logs(container), "")
290

Philip ABBET's avatar
Philip ABBET committed
291 292
    @slow
    def test_does_not_timeout(self):
293

294
        sleep_for = 0.5  # seconds
295

296
        container = self.host.create_container("debian:8.4", ["sleep", str(sleep_for)])
Philip ABBET's avatar
Philip ABBET committed
297
        self.host.start(container)
298

299
        status = self.host.wait(container, timeout=5)  # Should not timeout
300

301
        self.assertEqual(self.host.status(container), "exited")
Philip ABBET's avatar
Philip ABBET committed
302
        self.assertEqual(status, 0)
303
        self.assertEqual(self.host.logs(container), "")
304

305 306 307 308 309

class AsyncWithEnvironmentTest(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        cls.host = Host(raise_on_errors=False)
310
        cls.test_environment = cls.host.full_environment_name("Python for tests")
311 312 313 314 315 316 317

    @classmethod
    def tearDownClass(cls):
        cls.host.teardown()

    def tearDown(self):
        self.host.teardown()
Samuel GAIST's avatar
Samuel GAIST committed
318
        self.assertFalse(self.host.containers)  # All containers are gone
319

Philip ABBET's avatar
Philip ABBET committed
320 321
    @slow
    def test_memory_limit(self):
322

323 324 325 326 327 328 329 330 331 332 333 334
        cmd = [
            "python",
            "-c",
            "; ".join(
                [
                    "print('Before')",
                    "import sys; sys.stdout.flush()",
                    "d = '0' * (40 * 1024 * 1024)",
                    "import time; time.sleep(5)",
                    "print('After')",
                ]
            ),
Philip ABBET's avatar
Philip ABBET committed
335
        ]
336

Philip ABBET's avatar
Philip ABBET committed
337
        container = self.host.create_container(self.test_environment, cmd)
338 339 340 341 342 343
        # The amount of memory in megabytes should be greater than whatever
        # the docker process is started with (see:
        # https://unix.stackexchange.com/questions/412040/cgroups-memory-limit-write-error-device-or-resource-busy)
        # If you start seeing EBUSY (device or resource busy errors) from
        # docker, then try increasing a bit this value such that it still
        # triggers the memory allocation error for the array defined above.
344
        self.host.start(container, virtual_memory_in_megabytes=20)
345

Philip ABBET's avatar
Philip ABBET committed
346
        time.sleep(2)
347

Samuel GAIST's avatar
Samuel GAIST committed
348
        self.host.statistics(container)
349

Philip ABBET's avatar
Philip ABBET committed
350
        status = self.host.wait(container)
351

352
        self.assertEqual(self.host.status(container), "exited")
Philip ABBET's avatar
Philip ABBET committed
353
        self.assertEqual(status, 137)
354
        self.assertEqual(self.host.logs(container).strip(), "Before")
355

Philip ABBET's avatar
Philip ABBET committed
356 357
    @slow
    def test_memory_limit2(self):
358

359 360 361 362 363 364 365 366 367 368 369 370
        cmd = [
            "python",
            "-c",
            "; ".join(
                [
                    "print('Before')",
                    "import sys; sys.stdout.flush()",
                    "d = '0' * (10 * 1024 * 1024)",
                    "import time; time.sleep(5)",
                    "print('After')",
                ]
            ),
Philip ABBET's avatar
Philip ABBET committed
371
        ]
372

Philip ABBET's avatar
Philip ABBET committed
373 374
        container = self.host.create_container(self.test_environment, cmd)
        self.host.start(container, virtual_memory_in_megabytes=100)
375

Philip ABBET's avatar
Philip ABBET committed
376
        time.sleep(2)
377

Philip ABBET's avatar
Philip ABBET committed
378
        stats = self.host.statistics(container)
379

Philip ABBET's avatar
Philip ABBET committed
380
        status = self.host.wait(container)
381

Samuel GAIST's avatar
Samuel GAIST committed
382 383 384
        self.assertTrue(
            stats["memory"]["percent"] > 10,
            ("Memory check failed, " "%d%% <= 10%%" % stats["memory"]["percent"]),
385
        )
386

Samuel GAIST's avatar
Samuel GAIST committed
387 388 389
        self.assertTrue(
            stats["memory"]["percent"] < 20,
            ("Memory check failed, " "%d%% >= 15%%" % stats["memory"]["percent"]),
390
        )
391

392
        self.assertEqual(self.host.status(container), "exited")
Philip ABBET's avatar
Philip ABBET committed
393
        self.assertEqual(status, 0)
394
        self.assertEqual(self.host.logs(container).strip(), "Before\nAfter")
395

Philip ABBET's avatar
Philip ABBET committed
396
    def _run_cpulimit(self, processes, max_cpu_percent, sleep_time):
Samuel GAIST's avatar
Samuel GAIST committed
397
        tmp_folder = tempfile.gettempdir()
398
        program = pkg_resources.resource_filename(__name__, "cpu_stress.py")
Samuel GAIST's avatar
Samuel GAIST committed
399
        dst_name = os.path.join(tmp_folder, os.path.basename(program))
400

401 402 403
        container = self.host.create_container(
            self.test_environment, ["python", dst_name, str(processes)]
        )
404

Samuel GAIST's avatar
Samuel GAIST committed
405
        container.add_volume(program, os.path.join(tmp_folder, "cpu_stress.py"))
406

Philip ABBET's avatar
Philip ABBET committed
407
        self.host.start(container, max_cpu_percent=max_cpu_percent)
408

Philip ABBET's avatar
Philip ABBET committed
409
        time.sleep(sleep_time)
410

Philip ABBET's avatar
Philip ABBET committed
411
        stats = self.host.statistics(container)
412

413
        self.assertEqual(self.host.status(container), "running")
414

415
        percent = stats["cpu"]["percent"]
Samuel GAIST's avatar
Samuel GAIST committed
416 417 418 419 420 421
        self.assertTrue(
            percent < (1.1 * max_cpu_percent),
            (
                "%.2f%% is more than 20%% off the expected ceiling at %d%%!"
                % (percent, max_cpu_percent)
            ),
422
        )
423

Philip ABBET's avatar
Philip ABBET committed
424 425 426
        # make sure nothing is there anymore
        self.host.kill(container)
        self.assertEqual(self.host.wait(container), 137)
427

Philip ABBET's avatar
Philip ABBET committed
428 429 430 431
    @slow
    def test_cpulimit_at_20percent(self):
        # runs 1 process that should consume at most 20% of the host CPU
        self._run_cpulimit(1, 20, 3)
432

Philip ABBET's avatar
Philip ABBET committed
433 434 435 436
    @slow
    def test_cpulimit_at_100percent(self):
        # runs 4 processes that should consume 50% of the host CPU
        self._run_cpulimit(4, 100, 3)
437 438 439


class HostTest(unittest.TestCase):
Philip ABBET's avatar
Philip ABBET committed
440 441
    def setUp(self):
        Host.images_cache = {}
442

Philip ABBET's avatar
Philip ABBET committed
443 444 445
    @slow
    def test_images_cache(self):
        self.assertEqual(len(Host.images_cache), 0)
446

Philip ABBET's avatar
Philip ABBET committed
447 448
        # Might take some time
        start = time.time()
449

Philip ABBET's avatar
Philip ABBET committed
450 451
        host = Host(raise_on_errors=False)
        host.teardown()
452

Philip ABBET's avatar
Philip ABBET committed
453
        stop = time.time()
454

Philip ABBET's avatar
Philip ABBET committed
455 456
        nb_images = len(Host.images_cache)
        self.assertTrue(nb_images > 0)
457

458
        self.assertTrue(stop - start < 2.0)
459

Philip ABBET's avatar
Philip ABBET committed
460 461
        # Should be instantaneous
        start = time.time()
462

Philip ABBET's avatar
Philip ABBET committed
463 464
        host = Host(raise_on_errors=False)
        host.teardown()
465

Philip ABBET's avatar
Philip ABBET committed
466
        stop = time.time()
467

Philip ABBET's avatar
Philip ABBET committed
468
        self.assertEqual(len(Host.images_cache), nb_images)
469

Philip ABBET's avatar
Philip ABBET committed
470
        self.assertTrue(stop - start < 1.0)
471

Philip ABBET's avatar
Philip ABBET committed
472 473 474
    @slow
    def test_images_cache_file(self):
        self.assertEqual(len(Host.images_cache), 0)
475

Philip ABBET's avatar
Philip ABBET committed
476 477
        # Might take some time
        start = time.time()
478

479 480 481 482
        host = Host(
            images_cache=os.path.join(tmp_prefix, "images_cache.json"),
            raise_on_errors=False,
        )
Philip ABBET's avatar
Philip ABBET committed
483
        host.teardown()
484

Philip ABBET's avatar
Philip ABBET committed
485
        stop = time.time()
486

Philip ABBET's avatar
Philip ABBET committed
487 488
        nb_images = len(Host.images_cache)
        self.assertTrue(nb_images > 0)
489

490
        self.assertTrue(stop - start < 2.0)
491

Philip ABBET's avatar
Philip ABBET committed
492
        Host.images_cache = {}
493

Philip ABBET's avatar
Philip ABBET committed
494 495
        # Should be instantaneous
        start = time.time()
496

497 498 499 500
        host = Host(
            images_cache=os.path.join(tmp_prefix, "images_cache.json"),
            raise_on_errors=False,
        )
Philip ABBET's avatar
Philip ABBET committed
501
        host.teardown()
502

Philip ABBET's avatar
Philip ABBET committed
503
        stop = time.time()
504

Philip ABBET's avatar
Philip ABBET committed
505
        self.assertEqual(len(Host.images_cache), nb_images)
506

Philip ABBET's avatar
Philip ABBET committed
507
        self.assertTrue(stop - start < 1.0)