From 3ee30ed5e4fbfc54471ad4e591687a010322e9a7 Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Tue, 15 Sep 2015 20:48:33 +0200 Subject: [PATCH 1/4] Add support for cpu_quota and cpu_period in host_config Signed-off-by: Joffrey F --- docker/utils/utils.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/docker/utils/utils.py b/docker/utils/utils.py index 46b35160..bbea8e77 100644 --- a/docker/utils/utils.py +++ b/docker/utils/utils.py @@ -457,7 +457,8 @@ def create_host_config( restart_policy=None, cap_add=None, cap_drop=None, devices=None, extra_hosts=None, read_only=None, pid_mode=None, ipc_mode=None, security_opt=None, ulimits=None, log_config=None, mem_limit=None, - memswap_limit=None, cgroup_parent=None, group_add=None, version=None + memswap_limit=None, cgroup_parent=None, group_add=None, cpu_quota=None, + cpu_period=None, version=None ): host_config = {} @@ -601,6 +602,22 @@ def create_host_config( log_config = LogConfig(**log_config) host_config['LogConfig'] = log_config + if cpu_quota and not isinstance(cpu_quota, int): + raise TypeError( + 'Invalid type for cpu_quota param: expected int but' + ' found {0}'.format(type(cpu_quota)) + ) + elif cpu_quota: + host_config['CpuQuota'] = cpu_quota + + if cpu_period and not isinstance(cpu_period, int): + raise TypeError( + 'Invalid type for cpu_period param: expected int but' + ' found {0}'.format(type(cpu_period)) + ) + elif cpu_period: + host_config['CpuPeriod'] = cpu_period + return host_config From d89b2a01f05ab1be1a9e549f5db6b71ec41cf0c4 Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Tue, 15 Sep 2015 14:13:41 -0700 Subject: [PATCH 2/4] cpu_quota and cpu_period unit tests Signed-off-by: Joffrey F --- tests/utils_test.py | 279 ++++++++++++++++++++++++-------------------- 1 file changed, 155 insertions(+), 124 deletions(-) diff --git a/tests/utils_test.py b/tests/utils_test.py index b67ac4ec..45929f73 100644 --- a/tests/utils_test.py +++ b/tests/utils_test.py @@ -25,6 +25,159 @@ TEST_CERT_DIR = os.path.join( ) +class HostConfigTest(base.BaseTestCase): + def test_create_host_config_no_options(self): + config = create_host_config(version='1.19') + self.assertFalse('NetworkMode' in config) + + def test_create_host_config_no_options_newer_api_version(self): + config = create_host_config(version='1.20') + self.assertEqual(config['NetworkMode'], 'default') + + def test_create_host_config_invalid_cpu_cfs_types(self): + with pytest.raises(TypeError): + create_host_config(version='1.20', cpu_quota='0') + + with pytest.raises(TypeError): + create_host_config(version='1.20', cpu_period='0') + + with pytest.raises(TypeError): + create_host_config(version='1.20', cpu_quota=23.11) + + with pytest.raises(TypeError): + create_host_config(version='1.20', cpu_period=1999.0) + + def test_create_host_config_with_cpu_quota(self): + config = create_host_config(version='1.20', cpu_quota=1999) + self.assertEqual(config.get('CpuQuota'), 1999) + + def test_create_host_config_with_cpu_period(self): + config = create_host_config(version='1.20', cpu_period=1999) + self.assertEqual(config.get('CpuPeriod'), 1999) + + +class UlimitTest(base.BaseTestCase): + def test_create_host_config_dict_ulimit(self): + ulimit_dct = {'name': 'nofile', 'soft': 8096} + config = create_host_config( + ulimits=[ulimit_dct], version=DEFAULT_DOCKER_API_VERSION + ) + self.assertIn('Ulimits', config) + self.assertEqual(len(config['Ulimits']), 1) + ulimit_obj = config['Ulimits'][0] + self.assertTrue(isinstance(ulimit_obj, Ulimit)) + self.assertEqual(ulimit_obj.name, ulimit_dct['name']) + self.assertEqual(ulimit_obj.soft, ulimit_dct['soft']) + self.assertEqual(ulimit_obj['Soft'], ulimit_obj.soft) + + def test_create_host_config_dict_ulimit_capitals(self): + ulimit_dct = {'Name': 'nofile', 'Soft': 8096, 'Hard': 8096 * 4} + config = create_host_config( + ulimits=[ulimit_dct], version=DEFAULT_DOCKER_API_VERSION + ) + self.assertIn('Ulimits', config) + self.assertEqual(len(config['Ulimits']), 1) + ulimit_obj = config['Ulimits'][0] + self.assertTrue(isinstance(ulimit_obj, Ulimit)) + self.assertEqual(ulimit_obj.name, ulimit_dct['Name']) + self.assertEqual(ulimit_obj.soft, ulimit_dct['Soft']) + self.assertEqual(ulimit_obj.hard, ulimit_dct['Hard']) + self.assertEqual(ulimit_obj['Soft'], ulimit_obj.soft) + + def test_create_host_config_obj_ulimit(self): + ulimit_dct = Ulimit(name='nofile', soft=8096) + config = create_host_config( + ulimits=[ulimit_dct], version=DEFAULT_DOCKER_API_VERSION + ) + self.assertIn('Ulimits', config) + self.assertEqual(len(config['Ulimits']), 1) + ulimit_obj = config['Ulimits'][0] + self.assertTrue(isinstance(ulimit_obj, Ulimit)) + self.assertEqual(ulimit_obj, ulimit_dct) + + def test_ulimit_invalid_type(self): + self.assertRaises(ValueError, lambda: Ulimit(name=None)) + self.assertRaises(ValueError, lambda: Ulimit(name='hello', soft='123')) + self.assertRaises(ValueError, lambda: Ulimit(name='hello', hard='456')) + + +class LogConfigTest(base.BaseTestCase): + def test_create_host_config_dict_logconfig(self): + dct = {'type': LogConfig.types.SYSLOG, 'config': {'key1': 'val1'}} + config = create_host_config( + version=DEFAULT_DOCKER_API_VERSION, log_config=dct + ) + self.assertIn('LogConfig', config) + self.assertTrue(isinstance(config['LogConfig'], LogConfig)) + self.assertEqual(dct['type'], config['LogConfig'].type) + + def test_create_host_config_obj_logconfig(self): + obj = LogConfig(type=LogConfig.types.SYSLOG, config={'key1': 'val1'}) + config = create_host_config( + version=DEFAULT_DOCKER_API_VERSION, log_config=obj + ) + self.assertIn('LogConfig', config) + self.assertTrue(isinstance(config['LogConfig'], LogConfig)) + self.assertEqual(obj, config['LogConfig']) + + def test_logconfig_invalid_config_type(self): + with pytest.raises(ValueError): + LogConfig(type=LogConfig.types.JSON, config='helloworld') + + +class KwargsFromEnvTest(base.BaseTestCase): + def setUp(self): + self.os_environ = os.environ.copy() + + def tearDown(self): + os.environ = self.os_environ + + def test_kwargs_from_env_empty(self): + os.environ.update(DOCKER_HOST='', + DOCKER_CERT_PATH='', + DOCKER_TLS_VERIFY='') + + kwargs = kwargs_from_env() + self.assertEqual(None, kwargs.get('base_url')) + self.assertEqual(None, kwargs.get('tls')) + + def test_kwargs_from_env_tls(self): + os.environ.update(DOCKER_HOST='tcp://192.168.59.103:2376', + DOCKER_CERT_PATH=TEST_CERT_DIR, + DOCKER_TLS_VERIFY='1') + kwargs = kwargs_from_env(assert_hostname=False) + self.assertEqual('https://192.168.59.103:2376', kwargs['base_url']) + self.assertTrue('ca.pem' in kwargs['tls'].verify) + self.assertTrue('cert.pem' in kwargs['tls'].cert[0]) + self.assertTrue('key.pem' in kwargs['tls'].cert[1]) + self.assertEqual(False, kwargs['tls'].assert_hostname) + try: + client = Client(**kwargs) + self.assertEqual(kwargs['base_url'], client.base_url) + self.assertEqual(kwargs['tls'].verify, client.verify) + self.assertEqual(kwargs['tls'].cert, client.cert) + except TypeError as e: + self.fail(e) + + def test_kwargs_from_env_no_cert_path(self): + try: + temp_dir = tempfile.mkdtemp() + cert_dir = os.path.join(temp_dir, '.docker') + shutil.copytree(TEST_CERT_DIR, cert_dir) + + os.environ.update(HOME=temp_dir, + DOCKER_CERT_PATH='', + DOCKER_TLS_VERIFY='1') + + kwargs = kwargs_from_env() + self.assertIn(cert_dir, kwargs['tls'].verify) + self.assertIn(cert_dir, kwargs['tls'].cert[0]) + self.assertIn(cert_dir, kwargs['tls'].cert[1]) + finally: + if temp_dir: + shutil.rmtree(temp_dir) + + class UtilsTest(base.BaseTestCase): longMessage = True @@ -39,12 +192,6 @@ class UtilsTest(base.BaseTestCase): local_tempfile.close() return local_tempfile.name - def setUp(self): - self.os_environ = os.environ.copy() - - def tearDown(self): - os.environ = self.os_environ - def test_parse_repository_tag(self): self.assertEqual(parse_repository_tag("root"), ("root", None)) @@ -103,51 +250,6 @@ class UtilsTest(base.BaseTestCase): assert parse_host(val, 'win32') == tcp_port - def test_kwargs_from_env_empty(self): - os.environ.update(DOCKER_HOST='', - DOCKER_CERT_PATH='', - DOCKER_TLS_VERIFY='') - - kwargs = kwargs_from_env() - self.assertEqual(None, kwargs.get('base_url')) - self.assertEqual(None, kwargs.get('tls')) - - def test_kwargs_from_env_tls(self): - os.environ.update(DOCKER_HOST='tcp://192.168.59.103:2376', - DOCKER_CERT_PATH=TEST_CERT_DIR, - DOCKER_TLS_VERIFY='1') - kwargs = kwargs_from_env(assert_hostname=False) - self.assertEqual('https://192.168.59.103:2376', kwargs['base_url']) - self.assertTrue('ca.pem' in kwargs['tls'].verify) - self.assertTrue('cert.pem' in kwargs['tls'].cert[0]) - self.assertTrue('key.pem' in kwargs['tls'].cert[1]) - self.assertEqual(False, kwargs['tls'].assert_hostname) - try: - client = Client(**kwargs) - self.assertEqual(kwargs['base_url'], client.base_url) - self.assertEqual(kwargs['tls'].verify, client.verify) - self.assertEqual(kwargs['tls'].cert, client.cert) - except TypeError as e: - self.fail(e) - - def test_kwargs_from_env_no_cert_path(self): - try: - temp_dir = tempfile.mkdtemp() - cert_dir = os.path.join(temp_dir, '.docker') - shutil.copytree(TEST_CERT_DIR, cert_dir) - - os.environ.update(HOME=temp_dir, - DOCKER_CERT_PATH='', - DOCKER_TLS_VERIFY='1') - - kwargs = kwargs_from_env() - self.assertIn(cert_dir, kwargs['tls'].verify) - self.assertIn(cert_dir, kwargs['tls'].cert[0]) - self.assertIn(cert_dir, kwargs['tls'].cert[1]) - finally: - if temp_dir: - shutil.rmtree(temp_dir) - def test_parse_env_file_proper(self): env_file = self.generate_tempfile( file_content='USER=jdoe\nPASS=secret') @@ -181,79 +283,6 @@ class UtilsTest(base.BaseTestCase): for filters, expected in tests: self.assertEqual(convert_filters(filters), expected) - def test_create_host_config_no_options(self): - config = create_host_config(version='1.19') - self.assertFalse('NetworkMode' in config) - - def test_create_host_config_no_options_newer_api_version(self): - config = create_host_config(version='1.20') - self.assertEqual(config['NetworkMode'], 'default') - - def test_create_host_config_dict_ulimit(self): - ulimit_dct = {'name': 'nofile', 'soft': 8096} - config = create_host_config( - ulimits=[ulimit_dct], version=DEFAULT_DOCKER_API_VERSION - ) - self.assertIn('Ulimits', config) - self.assertEqual(len(config['Ulimits']), 1) - ulimit_obj = config['Ulimits'][0] - self.assertTrue(isinstance(ulimit_obj, Ulimit)) - self.assertEqual(ulimit_obj.name, ulimit_dct['name']) - self.assertEqual(ulimit_obj.soft, ulimit_dct['soft']) - self.assertEqual(ulimit_obj['Soft'], ulimit_obj.soft) - - def test_create_host_config_dict_ulimit_capitals(self): - ulimit_dct = {'Name': 'nofile', 'Soft': 8096, 'Hard': 8096 * 4} - config = create_host_config( - ulimits=[ulimit_dct], version=DEFAULT_DOCKER_API_VERSION - ) - self.assertIn('Ulimits', config) - self.assertEqual(len(config['Ulimits']), 1) - ulimit_obj = config['Ulimits'][0] - self.assertTrue(isinstance(ulimit_obj, Ulimit)) - self.assertEqual(ulimit_obj.name, ulimit_dct['Name']) - self.assertEqual(ulimit_obj.soft, ulimit_dct['Soft']) - self.assertEqual(ulimit_obj.hard, ulimit_dct['Hard']) - self.assertEqual(ulimit_obj['Soft'], ulimit_obj.soft) - - def test_create_host_config_obj_ulimit(self): - ulimit_dct = Ulimit(name='nofile', soft=8096) - config = create_host_config( - ulimits=[ulimit_dct], version=DEFAULT_DOCKER_API_VERSION - ) - self.assertIn('Ulimits', config) - self.assertEqual(len(config['Ulimits']), 1) - ulimit_obj = config['Ulimits'][0] - self.assertTrue(isinstance(ulimit_obj, Ulimit)) - self.assertEqual(ulimit_obj, ulimit_dct) - - def test_ulimit_invalid_type(self): - self.assertRaises(ValueError, lambda: Ulimit(name=None)) - self.assertRaises(ValueError, lambda: Ulimit(name='hello', soft='123')) - self.assertRaises(ValueError, lambda: Ulimit(name='hello', hard='456')) - - def test_create_host_config_dict_logconfig(self): - dct = {'type': LogConfig.types.SYSLOG, 'config': {'key1': 'val1'}} - config = create_host_config( - version=DEFAULT_DOCKER_API_VERSION, log_config=dct - ) - self.assertIn('LogConfig', config) - self.assertTrue(isinstance(config['LogConfig'], LogConfig)) - self.assertEqual(dct['type'], config['LogConfig'].type) - - def test_create_host_config_obj_logconfig(self): - obj = LogConfig(type=LogConfig.types.SYSLOG, config={'key1': 'val1'}) - config = create_host_config( - version=DEFAULT_DOCKER_API_VERSION, log_config=obj - ) - self.assertIn('LogConfig', config) - self.assertTrue(isinstance(config['LogConfig'], LogConfig)) - self.assertEqual(obj, config['LogConfig']) - - def test_logconfig_invalid_config_type(self): - with pytest.raises(ValueError): - LogConfig(type=LogConfig.types.JSON, config='helloworld') - def test_resolve_repository_name(self): # docker hub library image self.assertEqual( @@ -407,6 +436,8 @@ class UtilsTest(base.BaseTestCase): None, ) + +class PortsTest(base.BaseTestCase): def test_split_port_with_host_ip(self): internal_port, external_port = split_port("127.0.0.1:1000:2000") self.assertEqual(internal_port, ["2000"]) From 3c5185c199a0ac52c58eeca5ef9e2133e0f4803e Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Tue, 15 Sep 2015 14:29:46 -0700 Subject: [PATCH 3/4] Check API version when using cpu_period and cpu_quota Signed-off-by: Joffrey F --- docker/utils/utils.py | 34 +++++++++++++++++++++------------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/docker/utils/utils.py b/docker/utils/utils.py index bbea8e77..36edf8de 100644 --- a/docker/utils/utils.py +++ b/docker/utils/utils.py @@ -519,7 +519,7 @@ def create_host_config( host_config['Devices'] = parse_devices(devices) if group_add: - if compare_version(version, '1.20') < 0: + if version_lt(version, '1.20'): raise errors.InvalidVersion( 'group_add param not supported for API version < 1.20' ) @@ -602,20 +602,28 @@ def create_host_config( log_config = LogConfig(**log_config) host_config['LogConfig'] = log_config - if cpu_quota and not isinstance(cpu_quota, int): - raise TypeError( - 'Invalid type for cpu_quota param: expected int but' - ' found {0}'.format(type(cpu_quota)) - ) - elif cpu_quota: + if cpu_quota: + if not isinstance(cpu_quota, int): + raise TypeError( + 'Invalid type for cpu_quota param: expected int but' + ' found {0}'.format(type(cpu_quota)) + ) + if version_lt(version, '1.19'): + raise errors.InvalidVersion( + 'cpu_quota param not supported for API version < 1.19' + ) host_config['CpuQuota'] = cpu_quota - if cpu_period and not isinstance(cpu_period, int): - raise TypeError( - 'Invalid type for cpu_period param: expected int but' - ' found {0}'.format(type(cpu_period)) - ) - elif cpu_period: + if cpu_period: + if not isinstance(cpu_period, int): + raise TypeError( + 'Invalid type for cpu_period param: expected int but' + ' found {0}'.format(type(cpu_period)) + ) + if version_lt(version, '1.19'): + raise errors.InvalidVersion( + 'cpu_period param not supported for API version < 1.19' + ) host_config['CpuPeriod'] = cpu_period return host_config From 29b12cf007bf59c15ffbb6f508b41fc7eaa41c41 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Fri, 25 Sep 2015 20:10:16 +0200 Subject: [PATCH 4/4] _url can take arbitrarily many arguments Signed-off-by: Aanand Prasad --- docker/client.py | 23 ++++++++++++----------- tests/test.py | 30 +++++++++++++++++++++++++++--- 2 files changed, 39 insertions(+), 14 deletions(-) diff --git a/docker/client.py b/docker/client.py index 58d0496f..9decd610 100644 --- a/docker/client.py +++ b/docker/client.py @@ -111,21 +111,22 @@ class Client( def _delete(self, url, **kwargs): return self.delete(url, **self._set_request_timeout(kwargs)) - def _url(self, pathfmt, resource_id=None, versioned_api=True): - if resource_id and not isinstance(resource_id, six.string_types): - raise ValueError( - 'Expected a resource ID string but found {0} ({1}) ' - 'instead'.format(resource_id, type(resource_id)) - ) - elif resource_id: - resource_id = six.moves.urllib.parse.quote_plus(resource_id) + def _url(self, pathfmt, *args, **kwargs): + for arg in args: + if not isinstance(arg, six.string_types): + raise ValueError( + 'Expected a string but found {0} ({1}) ' + 'instead'.format(arg, type(arg)) + ) - if versioned_api: + args = map(six.moves.urllib.parse.quote_plus, args) + + if kwargs.get('versioned_api', True): return '{0}/v{1}{2}'.format( - self.base_url, self._version, pathfmt.format(resource_id) + self.base_url, self._version, pathfmt.format(*args) ) else: - return '{0}{1}'.format(self.base_url, pathfmt.format(resource_id)) + return '{0}{1}'.format(self.base_url, pathfmt.format(*args)) def _raise_for_status(self, response, explanation=None): """Raises stored :class:`APIError`, if one occurred.""" diff --git a/tests/test.py b/tests/test.py index 8ed5c14d..4a70b267 100644 --- a/tests/test.py +++ b/tests/test.py @@ -104,7 +104,9 @@ def fake_put(self, url, *args, **kwargs): def fake_delete(self, url, *args, **kwargs): return fake_request('DELETE', url, *args, **kwargs) -url_prefix = 'http+docker://localunixsocket/v{0}/'.format( +url_base = 'http+docker://localunixsocket/' +url_prefix = '{0}v{1}/'.format( + url_base, docker.constants.DEFAULT_DOCKER_API_VERSION) @@ -174,6 +176,14 @@ class DockerClientTest(Cleanup, base.BaseTestCase): url, '{0}{1}'.format(url_prefix, 'hello/somename/world') ) + url = self.client._url( + '/hello/{0}/world/{1}', 'somename', 'someothername' + ) + self.assertEqual( + url, + '{0}{1}'.format(url_prefix, 'hello/somename/world/someothername') + ) + url = self.client._url('/hello/{0}/world', '/some?name') self.assertEqual( url, '{0}{1}'.format(url_prefix, 'hello/%2Fsome%3Fname/world') @@ -187,8 +197,13 @@ class DockerClientTest(Cleanup, base.BaseTestCase): url = self.client._url('/simple') self.assertEqual(url, '{0}{1}'.format(url_prefix, 'simple')) - url = self.client._url('/simple', None) - self.assertEqual(url, '{0}{1}'.format(url_prefix, 'simple')) + def test_url_unversioned_api(self): + url = self.client._url( + '/hello/{0}/world', 'somename', versioned_api=False + ) + self.assertEqual( + url, '{0}{1}'.format(url_base, 'hello/somename/world') + ) ######################### # INFORMATION TESTS # @@ -202,6 +217,15 @@ class DockerClientTest(Cleanup, base.BaseTestCase): timeout=DEFAULT_TIMEOUT_SECONDS ) + def test_version_no_api_version(self): + self.client.version(False) + + fake_request.assert_called_with( + 'GET', + url_base + 'version', + timeout=DEFAULT_TIMEOUT_SECONDS + ) + def test_retrieve_server_version(self): client = docker.Client(version="auto") self.assertTrue(isinstance(client._version, six.string_types))