test_docker.py 13 KB
Newer Older
André Anjos's avatar
André Anjos committed
1 2 3 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
#!/usr/bin/env python
# vim: set fileencoding=utf-8 :

###############################################################################
#                                                                             #
# Copyright (c) 2016 Idiap Research Institute, http://www.idiap.ch/           #
# Contact: beat.support@idiap.ch                                              #
#                                                                             #
# This file is part of the beat.core module of the BEAT platform.             #
#                                                                             #
# Commercial License Usage                                                    #
# Licensees holding valid commercial BEAT licenses may use this file in       #
# accordance with the terms contained in a written agreement between you      #
# and Idiap. For further information contact tto@idiap.ch                     #
#                                                                             #
# Alternatively, this file may be used under the terms of the GNU Affero      #
# Public License version 3 as published by the Free Software and appearing    #
# in the file LICENSE.AGPL included in the packaging of this file.            #
# The BEAT platform is distributed in the hope that it will be useful, but    #
# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY  #
# or FITNESS FOR A PARTICULAR PURPOSE.                                        #
#                                                                             #
# You should have received a copy of the GNU Affero Public License along      #
# with the BEAT platform. If not, see http://www.gnu.org/licenses/.           #
#                                                                             #
###############################################################################


"""Asynchronous process I/O with the Subprocess module
"""

import os
import sys
import time
35
import unittest
André Anjos's avatar
André Anjos committed
36
import pkg_resources
37
import time
André Anjos's avatar
André Anjos committed
38

39
import requests
40
import nose
André Anjos's avatar
André Anjos committed
41

42 43
from tempfile import TemporaryDirectory

44
from ..dock import Host
45

46
from . import tmp_prefix
47
from .utils import slow
48 49
from .utils import skipif

50
from . import network_name
51
from . import DOCKER_NETWORK_TEST_ENABLED
52

53 54 55 56
class NoDiscoveryTests(unittest.TestCase):
    """Test cases that don't require the discovery of database and runtime
    environments.
    """
57

Philip ABBET's avatar
Philip ABBET committed
58 59
    @classmethod
    def setUpClass(cls):
60
        cls.host = Host(raise_on_errors=False, discover=False)
61 62


Philip ABBET's avatar
Philip ABBET committed
63 64 65
    @classmethod
    def tearDownClass(cls):
        cls.host.teardown()
66 67


Philip ABBET's avatar
Philip ABBET committed
68 69 70
    def tearDown(self):
        self.host.teardown()
        assert not self.host.containers # All containers are gone
71 72


73 74 75
class NetworkTest(NoDiscoveryTests):

    @slow
76
    @skipif(not DOCKER_NETWORK_TEST_ENABLED, "Network test disabled")
77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93
    def test_network(self):
        string = "hello world"
        container = self.host.create_container('debian:8.4', ["echo", string])
        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)
        self.assertEqual(self.host.logs(container), string + '\n')


    @slow
94
    @skipif(not DOCKER_NETWORK_TEST_ENABLED, "Network test disabled")
95 96 97 98 99 100 101 102 103 104 105
    def test_non_existing_network(self):

        string = "hello world"
        network_name = 'beat.core.fake'
        container = self.host.create_container('debian:8.4', ["echo", string])
        container.network_name = network_name

        try:
            self.host.start(container)
        except RuntimeError as e:
            self.assertTrue(str(e).find('network %s not found' % network_name) >= 0)
106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124


class UserTest(NoDiscoveryTests):

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

        container = self.host.create_container('debian:8.4', ["id"])
        container.uid = 10000

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

        self.assertEqual(status, 0)
        self.assertTrue(self.host.logs(container).startswith('uid={0} gid={0}'.format(container.uid)))


125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141
class EnvironmentVariableTest(NoDiscoveryTests):

    @slow
    def test_environment_variable(self):
        """Test that the uid property is correctly used.
        """

        container = self.host.create_container('debian:8.4', ["env"])
        container.add_environment_variable('DOCKER_TEST', 'good')

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

        self.assertEqual(status, 0)
        self.assertTrue('DOCKER_TEST=good' in self.host.logs(container))


142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166
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"
            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')

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


167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186
class EntrypointTest(NoDiscoveryTests):

    @slow
    def test_entrypoint(self):
        """Test that the entrypoint property is correctly used.
        """

        container = self.host.create_container('debian:8.4', ["42"])
        container.set_entrypoint('echo')

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


187 188
class AsyncTest(NoDiscoveryTests):

Philip ABBET's avatar
Philip ABBET committed
189 190
    @slow
    def test_echo(self):
191

Philip ABBET's avatar
Philip ABBET committed
192
        string = "hello, world"
193

Philip ABBET's avatar
Philip ABBET committed
194 195 196
        container = self.host.create_container('debian:8.4', ["echo", string])
        self.host.start(container)
        status = self.host.wait(container)
197

Philip ABBET's avatar
Philip ABBET committed
198
        self.assertEqual(status, 0)
199
        self.assertEqual(self.host.logs(container), string + '\n')
200

Philip ABBET's avatar
Philip ABBET committed
201 202
    @slow
    def test_non_existing(self):
203

Philip ABBET's avatar
Philip ABBET committed
204
        container = self.host.create_container('debian:8.4', ["sdfsdfdsf329909092"])
205

206 207
        try:
            self.host.start(container)
208
        except Exception as e:
209
            self.assertTrue(str(e).find('Failed to create the container') >= 0)
210 211

        self.assertFalse(self.host.containers) # All containers are gone
212 213


Philip ABBET's avatar
Philip ABBET committed
214 215
    @slow
    def test_timeout(self):
216

Philip ABBET's avatar
Philip ABBET committed
217
        sleep_for = 100 # seconds
218

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

222 223
        retval = self.host.wait(container, timeout=0.5)
        self.assertTrue(retval is None)
224

Philip ABBET's avatar
Philip ABBET committed
225
        self.host.kill(container)
226

227
        retval = self.host.wait(container)
228

Philip ABBET's avatar
Philip ABBET committed
229
        self.assertEqual(self.host.status(container), 'exited')
230 231
        self.assertEqual(retval, 137)
        self.assertEqual(self.host.logs(container), '')
232 233


Philip ABBET's avatar
Philip ABBET committed
234 235
    @slow
    def test_does_not_timeout(self):
236

Philip ABBET's avatar
Philip ABBET committed
237
        sleep_for = 0.5 # seconds
238

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

Philip ABBET's avatar
Philip ABBET committed
242
        status = self.host.wait(container, timeout=5) # Should not timeout
243

Philip ABBET's avatar
Philip ABBET committed
244 245
        self.assertEqual(self.host.status(container), 'exited')
        self.assertEqual(status, 0)
246
        self.assertEqual(self.host.logs(container), '')
247 248


249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265

class AsyncWithEnvironmentTest(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        cls.host = Host(raise_on_errors=False)
        cls.test_environment = cls.host.full_environment_name('Python 2.7')


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


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

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

Philip ABBET's avatar
Philip ABBET committed
269 270 271
        cmd = ['python', '-c', '; '.join([
            "print('Before')",
            "import sys; sys.stdout.flush()",
272
            "d = '0' * (40 * 1024 * 1024)",
Philip ABBET's avatar
Philip ABBET committed
273 274 275 276
            "import time; time.sleep(5)",
            "print('After')",
          ])
        ]
277

Philip ABBET's avatar
Philip ABBET committed
278
        container = self.host.create_container(self.test_environment, cmd)
279 280 281 282 283 284
        # 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.
285
        self.host.start(container, virtual_memory_in_megabytes=20)
286

Philip ABBET's avatar
Philip ABBET committed
287
        time.sleep(2)
288

Philip ABBET's avatar
Philip ABBET committed
289
        stats = self.host.statistics(container)
290

Philip ABBET's avatar
Philip ABBET committed
291
        status = self.host.wait(container)
292

Philip ABBET's avatar
Philip ABBET committed
293 294
        self.assertEqual(self.host.status(container), 'exited')
        self.assertEqual(status, 137)
295
        self.assertEqual(self.host.logs(container).strip(), 'Before')
296 297


Philip ABBET's avatar
Philip ABBET committed
298 299
    @slow
    def test_memory_limit2(self):
300

Philip ABBET's avatar
Philip ABBET committed
301 302 303 304 305 306 307 308
        cmd = ['python', '-c', '; '.join([
            "print('Before')",
            "import sys; sys.stdout.flush()",
            "d = '0' * (10 * 1024 * 1024)",
            "import time; time.sleep(5)",
            "print('After')",
          ])
        ]
309

Philip ABBET's avatar
Philip ABBET committed
310 311
        container = self.host.create_container(self.test_environment, cmd)
        self.host.start(container, virtual_memory_in_megabytes=100)
312

Philip ABBET's avatar
Philip ABBET committed
313
        time.sleep(2)
314

Philip ABBET's avatar
Philip ABBET committed
315
        stats = self.host.statistics(container)
316

Philip ABBET's avatar
Philip ABBET committed
317
        status = self.host.wait(container)
318

Philip ABBET's avatar
Philip ABBET committed
319 320
        assert stats['memory']['percent'] > 10, 'Memory check failed, ' \
            '%d%% <= 10%%' % stats['memory']['percent']
321

322
        assert stats['memory']['percent'] < 20, 'Memory check failed, ' \
Philip ABBET's avatar
Philip ABBET committed
323
            '%d%% >= 15%%' % stats['memory']['percent']
324

Philip ABBET's avatar
Philip ABBET committed
325 326
        self.assertEqual(self.host.status(container), 'exited')
        self.assertEqual(status, 0)
327
        self.assertEqual(self.host.logs(container).strip(), 'Before\nAfter')
328 329


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

Philip ABBET's avatar
Philip ABBET committed
332 333
        program = pkg_resources.resource_filename(__name__, 'cpu_stress.py')
        dst_name = os.path.join('/tmp', os.path.basename(program))
334

Philip ABBET's avatar
Philip ABBET committed
335 336
        container = self.host.create_container(self.test_environment,
                                               ['python', dst_name, str(processes)])
337

338
        container.add_volume(program, os.path.join('/tmp', 'cpu_stress.py'))
339

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

Philip ABBET's avatar
Philip ABBET committed
342
        time.sleep(sleep_time)
343

Philip ABBET's avatar
Philip ABBET committed
344
        stats = self.host.statistics(container)
345

Philip ABBET's avatar
Philip ABBET committed
346
        self.assertEqual(self.host.status(container), 'running')
347

Philip ABBET's avatar
Philip ABBET committed
348 349 350
        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)
351

Philip ABBET's avatar
Philip ABBET committed
352 353 354
        # make sure nothing is there anymore
        self.host.kill(container)
        self.assertEqual(self.host.wait(container), 137)
355 356


Philip ABBET's avatar
Philip ABBET committed
357 358 359 360
    @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)
361 362


Philip ABBET's avatar
Philip ABBET committed
363 364 365 366
    @slow
    def test_cpulimit_at_100percent(self):
        # runs 4 processes that should consume 50% of the host CPU
        self._run_cpulimit(4, 100, 3)
367 368 369 370 371



class HostTest(unittest.TestCase):

Philip ABBET's avatar
Philip ABBET committed
372 373
    def setUp(self):
        Host.images_cache = {}
374 375


Philip ABBET's avatar
Philip ABBET committed
376 377 378
    @slow
    def test_images_cache(self):
        self.assertEqual(len(Host.images_cache), 0)
379

Philip ABBET's avatar
Philip ABBET committed
380 381
        # Might take some time
        start = time.time()
382

Philip ABBET's avatar
Philip ABBET committed
383 384
        host = Host(raise_on_errors=False)
        host.teardown()
385

Philip ABBET's avatar
Philip ABBET committed
386
        stop = time.time()
387

Philip ABBET's avatar
Philip ABBET committed
388 389
        nb_images = len(Host.images_cache)
        self.assertTrue(nb_images > 0)
390

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

Philip ABBET's avatar
Philip ABBET committed
393 394
        # Should be instantaneous
        start = time.time()
395

Philip ABBET's avatar
Philip ABBET committed
396 397
        host = Host(raise_on_errors=False)
        host.teardown()
398

Philip ABBET's avatar
Philip ABBET committed
399
        stop = time.time()
400

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

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


Philip ABBET's avatar
Philip ABBET committed
406 407 408
    @slow
    def test_images_cache_file(self):
        self.assertEqual(len(Host.images_cache), 0)
409

Philip ABBET's avatar
Philip ABBET committed
410 411
        # Might take some time
        start = time.time()
412

Philip ABBET's avatar
Philip ABBET committed
413 414 415
        host = Host(images_cache=os.path.join(tmp_prefix, 'images_cache.json'),
                    raise_on_errors=False)
        host.teardown()
416

Philip ABBET's avatar
Philip ABBET committed
417
        stop = time.time()
418

Philip ABBET's avatar
Philip ABBET committed
419 420
        nb_images = len(Host.images_cache)
        self.assertTrue(nb_images > 0)
421

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

Philip ABBET's avatar
Philip ABBET committed
424
        Host.images_cache = {}
425

Philip ABBET's avatar
Philip ABBET committed
426 427
        # Should be instantaneous
        start = time.time()
428

Philip ABBET's avatar
Philip ABBET committed
429 430 431
        host = Host(images_cache=os.path.join(tmp_prefix, 'images_cache.json'),
                    raise_on_errors=False)
        host.teardown()
432

Philip ABBET's avatar
Philip ABBET committed
433
        stop = time.time()
434

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

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