test_docker.py 14.1 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 41

import os
import sys
import time
42
import unittest
André Anjos's avatar
André Anjos committed
43
import pkg_resources
44
import time
André Anjos's avatar
André Anjos committed
45

46
import requests
47
import nose
André Anjos's avatar
André Anjos committed
48

49 50
from tempfile import TemporaryDirectory

51
from ..dock import Host
52

53
from . import tmp_prefix
54
from .utils import slow
55 56
from .utils import skipif

57
from . import network_name
58
from . import DOCKER_NETWORK_TEST_ENABLED
59

60

61 62 63 64
class NoDiscoveryTests(unittest.TestCase):
    """Test cases that don't require the discovery of database and runtime
    environments.
    """
65

Philip ABBET's avatar
Philip ABBET committed
66 67
    @classmethod
    def setUpClass(cls):
68
        cls.host = Host(raise_on_errors=False, discover=False)
69

Philip ABBET's avatar
Philip ABBET committed
70 71 72
    @classmethod
    def tearDownClass(cls):
        cls.host.teardown()
73

Philip ABBET's avatar
Philip ABBET committed
74 75
    def tearDown(self):
        self.host.teardown()
76
        assert not self.host.containers  # All containers are gone
77 78


79 80
class NetworkTest(NoDiscoveryTests):
    @slow
81
    @skipif(not DOCKER_NETWORK_TEST_ENABLED, "Network test disabled")
82 83
    def test_network(self):
        string = "hello world"
84
        container = self.host.create_container("debian:8.4", ["echo", string])
85 86 87 88 89 90 91 92 93 94
        container.network_name = network_name

        try:
            self.host.start(container)
            status = self.host.wait(container)
        except Exception as e:
            network.remove()
            raise

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

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

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

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


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

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

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

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


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

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

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

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


148 149 150 151 152 153 154 155
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"
156 157 158 159 160
            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")
161 162 163 164 165 166 167 168 169 170 171 172 173

            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")


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

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

        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")


193
class AsyncTest(NoDiscoveryTests):
Philip ABBET's avatar
Philip ABBET committed
194 195
    @slow
    def test_echo(self):
196

Philip ABBET's avatar
Philip ABBET committed
197
        string = "hello, world"
198

199
        container = self.host.create_container("debian:8.4", ["echo", string])
Philip ABBET's avatar
Philip ABBET committed
200 201
        self.host.start(container)
        status = self.host.wait(container)
202

Philip ABBET's avatar
Philip ABBET committed
203
        self.assertEqual(status, 0)
204
        self.assertEqual(self.host.logs(container), string + "\n")
205

Philip ABBET's avatar
Philip ABBET committed
206 207
    @slow
    def test_non_existing(self):
208

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

211 212
        try:
            self.host.start(container)
213
        except Exception as e:
214
            self.assertTrue(str(e).find("Failed to create the container") >= 0)
215

216
        self.assertFalse(self.host.containers)  # All containers are gone
217

Philip ABBET's avatar
Philip ABBET committed
218 219
    @slow
    def test_timeout(self):
220

221
        sleep_for = 100  # seconds
222

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

226 227
        retval = self.host.wait(container, timeout=0.5)
        self.assertTrue(retval is None)
228

Philip ABBET's avatar
Philip ABBET committed
229
        self.host.kill(container)
230

231
        retval = self.host.wait(container)
232

233
        self.assertEqual(self.host.status(container), "exited")
234
        self.assertEqual(retval, 137)
235
        self.assertEqual(self.host.logs(container), "")
236

Philip ABBET's avatar
Philip ABBET committed
237 238
    @slow
    def test_does_not_timeout(self):
239

240
        sleep_for = 0.5  # seconds
241

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

245
        status = self.host.wait(container, timeout=5)  # Should not timeout
246

247
        self.assertEqual(self.host.status(container), "exited")
Philip ABBET's avatar
Philip ABBET committed
248
        self.assertEqual(status, 0)
249
        self.assertEqual(self.host.logs(container), "")
250

251 252 253 254 255

class AsyncWithEnvironmentTest(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        cls.host = Host(raise_on_errors=False)
256
        cls.test_environment = cls.host.full_environment_name("Python 2.7")
257 258 259 260 261 262 263

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

    def tearDown(self):
        self.host.teardown()
264
        assert not self.host.containers  # All containers are gone
265

Philip ABBET's avatar
Philip ABBET committed
266 267
    @slow
    def test_memory_limit(self):
268

269 270 271 272 273 274 275 276 277 278 279 280
        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
281
        ]
282

Philip ABBET's avatar
Philip ABBET committed
283
        container = self.host.create_container(self.test_environment, cmd)
284 285 286 287 288 289
        # 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.
290
        self.host.start(container, virtual_memory_in_megabytes=20)
291

Philip ABBET's avatar
Philip ABBET committed
292
        time.sleep(2)
293

Philip ABBET's avatar
Philip ABBET committed
294
        stats = self.host.statistics(container)
295

Philip ABBET's avatar
Philip ABBET committed
296
        status = self.host.wait(container)
297

298
        self.assertEqual(self.host.status(container), "exited")
Philip ABBET's avatar
Philip ABBET committed
299
        self.assertEqual(status, 137)
300
        self.assertEqual(self.host.logs(container).strip(), "Before")
301

Philip ABBET's avatar
Philip ABBET committed
302 303
    @slow
    def test_memory_limit2(self):
304

305 306 307 308 309 310 311 312 313 314 315 316
        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
317
        ]
318

Philip ABBET's avatar
Philip ABBET committed
319 320
        container = self.host.create_container(self.test_environment, cmd)
        self.host.start(container, virtual_memory_in_megabytes=100)
321

Philip ABBET's avatar
Philip ABBET committed
322
        time.sleep(2)
323

Philip ABBET's avatar
Philip ABBET committed
324
        stats = self.host.statistics(container)
325

Philip ABBET's avatar
Philip ABBET committed
326
        status = self.host.wait(container)
327

328 329 330
        assert stats["memory"]["percent"] > 10, (
            "Memory check failed, " "%d%% <= 10%%" % stats["memory"]["percent"]
        )
331

332 333 334
        assert stats["memory"]["percent"] < 20, (
            "Memory check failed, " "%d%% >= 15%%" % stats["memory"]["percent"]
        )
335

336
        self.assertEqual(self.host.status(container), "exited")
Philip ABBET's avatar
Philip ABBET committed
337
        self.assertEqual(status, 0)
338
        self.assertEqual(self.host.logs(container).strip(), "Before\nAfter")
339

Philip ABBET's avatar
Philip ABBET committed
340
    def _run_cpulimit(self, processes, max_cpu_percent, sleep_time):
341

342 343
        program = pkg_resources.resource_filename(__name__, "cpu_stress.py")
        dst_name = os.path.join("/tmp", os.path.basename(program))
344

345 346 347
        container = self.host.create_container(
            self.test_environment, ["python", dst_name, str(processes)]
        )
348

349
        container.add_volume(program, os.path.join("/tmp", "cpu_stress.py"))
350

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

Philip ABBET's avatar
Philip ABBET committed
353
        time.sleep(sleep_time)
354

Philip ABBET's avatar
Philip ABBET committed
355
        stats = self.host.statistics(container)
356

357
        self.assertEqual(self.host.status(container), "running")
358

359 360 361 362 363
        percent = stats["cpu"]["percent"]
        assert percent < (1.1 * max_cpu_percent), (
            "%.2f%% is more than 20%% off the expected ceiling at %d%%!"
            % (percent, max_cpu_percent)
        )
364

Philip ABBET's avatar
Philip ABBET committed
365 366 367
        # make sure nothing is there anymore
        self.host.kill(container)
        self.assertEqual(self.host.wait(container), 137)
368

Philip ABBET's avatar
Philip ABBET committed
369 370 371 372
    @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)
373

Philip ABBET's avatar
Philip ABBET committed
374 375 376 377
    @slow
    def test_cpulimit_at_100percent(self):
        # runs 4 processes that should consume 50% of the host CPU
        self._run_cpulimit(4, 100, 3)
378 379 380


class HostTest(unittest.TestCase):
Philip ABBET's avatar
Philip ABBET committed
381 382
    def setUp(self):
        Host.images_cache = {}
383

Philip ABBET's avatar
Philip ABBET committed
384 385 386
    @slow
    def test_images_cache(self):
        self.assertEqual(len(Host.images_cache), 0)
387

Philip ABBET's avatar
Philip ABBET committed
388 389
        # Might take some time
        start = time.time()
390

Philip ABBET's avatar
Philip ABBET committed
391 392
        host = Host(raise_on_errors=False)
        host.teardown()
393

Philip ABBET's avatar
Philip ABBET committed
394
        stop = time.time()
395

Philip ABBET's avatar
Philip ABBET committed
396 397
        nb_images = len(Host.images_cache)
        self.assertTrue(nb_images > 0)
398

Philip ABBET's avatar
Philip ABBET committed
399
        self.assertTrue(stop - start > 2.0)
400

Philip ABBET's avatar
Philip ABBET committed
401 402
        # Should be instantaneous
        start = time.time()
403

Philip ABBET's avatar
Philip ABBET committed
404 405
        host = Host(raise_on_errors=False)
        host.teardown()
406

Philip ABBET's avatar
Philip ABBET committed
407
        stop = time.time()
408

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

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

Philip ABBET's avatar
Philip ABBET committed
413 414 415
    @slow
    def test_images_cache_file(self):
        self.assertEqual(len(Host.images_cache), 0)
416

Philip ABBET's avatar
Philip ABBET committed
417 418
        # Might take some time
        start = time.time()
419

420 421 422 423
        host = Host(
            images_cache=os.path.join(tmp_prefix, "images_cache.json"),
            raise_on_errors=False,
        )
Philip ABBET's avatar
Philip ABBET committed
424
        host.teardown()
425

Philip ABBET's avatar
Philip ABBET committed
426
        stop = time.time()
427

Philip ABBET's avatar
Philip ABBET committed
428 429
        nb_images = len(Host.images_cache)
        self.assertTrue(nb_images > 0)
430

Philip ABBET's avatar
Philip ABBET committed
431
        self.assertTrue(stop - start > 2.0)
432

Philip ABBET's avatar
Philip ABBET committed
433
        Host.images_cache = {}
434

Philip ABBET's avatar
Philip ABBET committed
435 436
        # Should be instantaneous
        start = time.time()
437

438 439 440 441
        host = Host(
            images_cache=os.path.join(tmp_prefix, "images_cache.json"),
            raise_on_errors=False,
        )
Philip ABBET's avatar
Philip ABBET committed
442
        host.teardown()
443

Philip ABBET's avatar
Philip ABBET committed
444
        stop = time.time()
445

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

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