From a451119e4a9cbb640ed20c3ff30224e3435c58af Mon Sep 17 00:00:00 2001 From: Maxime Petazzoni Date: Fri, 8 Nov 2013 13:50:19 -0800 Subject: [PATCH] Allow for configurable timeout on all client requests Signed-off-by: Maxime Petazzoni --- README.md | 3 +- docker/client.py | 90 +++++++++++++++++++++++-------------- docker/unixconn/unixconn.py | 22 +++++---- tests/integration_test.py | 28 ++++++++++-- tests/test.py | 72 +++++++++++++++++++---------- 5 files changed, 145 insertions(+), 70 deletions(-) diff --git a/README.md b/README.md index 7082ba43..10013d1d 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,8 @@ An API client for docker written in Python API === -`docker.Client(base_url='unix://var/run/docker.sock', version="1.4")` +`docker.Client(base_url='unix://var/run/docker.sock', version="1.4", +timeout=60)` Client class. `base_url` refers to the protocol+hostname+port where the docker server is hosted. Version is the version of the API the client will use. diff --git a/docker/client.py b/docker/client.py index 771feb3c..3fccd812 100644 --- a/docker/client.py +++ b/docker/client.py @@ -27,6 +27,7 @@ import docker.utils as utils if not six.PY3: import websocket +DEFAULT_TIMEOUT_SECONDS = 60 class APIError(requests.exceptions.HTTPError): def __init__(self, message, response, explanation=None): @@ -61,13 +62,17 @@ class APIError(requests.exceptions.HTTPError): class Client(requests.Session): - def __init__(self, base_url="unix://var/run/docker.sock", version="1.4"): + + def __init__(self, base_url="unix://var/run/docker.sock", version="1.4", + timeout=DEFAULT_TIMEOUT_SECONDS): super(Client, self).__init__() if base_url.startswith('unix:///'): base_url = base_url.replace('unix:/', 'unix:') - self.mount('unix://', unixconn.UnixAdapter(base_url)) self.base_url = base_url self._version = version + self._timeout = timeout + + self.mount('unix://', unixconn.UnixAdapter(base_url, timeout)) try: self._cfg = auth.load_config() except Exception: @@ -143,7 +148,8 @@ class Client(requests.Session): if 'headers' not in kwargs: kwargs['headers'] = {} kwargs['headers']['Content-Type'] = 'application/json' - return self.post(url, json.dumps(data2), **kwargs) + return self.post(url, json.dumps(data2), + timeout=self._timeout, **kwargs) def attach_socket(self, container, params=None, ws=False): if ws: @@ -153,7 +159,7 @@ class Client(requests.Session): container = container.get('Id') u = self._url("/containers/{0}/attach".format(container)) res = self.post(u, None, params=self._attach_params(params), - stream=True) + stream=True, timeout=self._timeout) self._raise_for_status(res) # hijack the underlying socket from requests, icky # but for some reason requests.iter_contents and ilk @@ -215,7 +221,8 @@ class Client(requests.Session): if context is not None: headers = {'Content-Type': 'application/tar'} res = self._result(self.post(u, context, params=params, - headers=headers, stream=True)) + headers=headers, stream=True, + timeout=self._timeout)) if context is not None: context.close() srch = r'Successfully built ([0-9a-f]+)' @@ -246,7 +253,8 @@ class Client(requests.Session): 'before': before } u = self._url("/containers/ps") - res = self._result(self.get(u, params=params), True) + res = self._result(self.get(u, params=params, timeout=self._timeout), + True) if quiet: return [{'Id': x['Id']} for x in res] return res @@ -284,30 +292,33 @@ class Client(requests.Session): if isinstance(container, dict): container = container.get('Id') return self._result(self.get(self._url("/containers/{0}/changes". - format(container))), True) + format(container)), timeout=self._timeout), True) def export(self, container): if isinstance(container, dict): container = container.get('Id') res = self.get(self._url("/containers/{0}/export".format(container)), - stream=True) + stream=True, timeout=self._timeout) self._raise_for_status(res) return res.raw def history(self, image): - res = self.get(self._url("/images/{0}/history".format(image))) + res = self.get(self._url("/images/{0}/history".format(image)), + timeout=self._timeout) self._raise_for_status(res) return self._result(res) def images(self, name=None, quiet=False, all=False, viz=False): if viz: - return self._result(self.get(self._url("images/viz"))) + return self._result(self.get(self._url("images/viz"), + timeout=self._timeout)) params = { 'filter': name, 'only_ids': 1 if quiet else 0, 'all': 1 if all else 0, } - res = self._result(self.get(self._url("/images/json"), params=params), + res = self._result(self.get(self._url("/images/json"), params=params, + timeout=self._timeout), True) if quiet: return [x['Id'] for x in res] @@ -331,12 +342,16 @@ class Client(requests.Session): data = None if isinstance(src, six.string_types): params['fromSrc'] = src - return self._result(self.post(u, data, params=params)) + return self._result(self.post(u, data, params=params, + timeout=self._timeout)) - return self._result(self.post(u, src, params=params)) + return self._result(self.post(u, src, params=params, + timeout=self._timeout)) def info(self): - return self._result(self.get(self._url("/info")), True) + return self._result(self.get(self._url("/info"), + timeout=self._timeout), + True) def insert(self, image, url, path): api_url = self._url("/images/" + image + "/insert") @@ -344,25 +359,26 @@ class Client(requests.Session): 'url': url, 'path': path } - return self._result(self.post(api_url, None, params=params)) + return self._result(self.post(api_url, None, params=params, + timeout=self._timeout)) def inspect_container(self, container): if isinstance(container, dict): container = container.get('Id') - return self._result(self.get( - self._url("/containers/{0}/json".format(container)) - ), True) + return self._result(self.get(self._url("/containers/{0}/json".format(container)), + timeout=self._timeout), + True) def inspect_image(self, image_id): - return self._result(self.get( - self._url("/images/{0}/json".format(image_id)) - ), True) + return self._result(self.get(self._url("/images/{0}/json".format(image_id)), + timeout=self._timeout), + True) def kill(self, container): if isinstance(container, dict): container = container.get('Id') url = self._url("/containers/{0}/kill".format(container)) - res = self.post(url, None) + res = self.post(url, None, timeout=self._timeout) self._raise_for_status(res) def login(self, username, password=None, email=None, registry=None): @@ -393,12 +409,14 @@ class Client(requests.Session): 'stderr': 1 } u = self._url("/containers/{0}/attach".format(container)) - return self._result(self.post(u, None, params=params)) + return self._result(self.post(u, None, params=params, + timeout=self._timeout)) def port(self, container, private_port): if isinstance(container, dict): container = container.get('Id') - res = self.get(self._url("/containers/{0}/json".format(container))) + res = self.get(self._url("/containers/{0}/json".format(container)), + timeout=self._timeout) self._raise_for_status(res) json_ = res.json() s_port = str(private_port) @@ -430,7 +448,8 @@ class Client(requests.Session): if authcfg: headers['X-Registry-Auth'] = auth.encode_header(authcfg) u = self._url("/images/create") - return self._result(self.post(u, params=params, headers=headers)) + return self._result(self.post(u, params=params, headers=headers, + timeout=self._timeout)) def push(self, repository): registry, repo_name = auth.resolve_repository_name(repository) @@ -451,11 +470,12 @@ class Client(requests.Session): if isinstance(container, dict): container = container.get('Id') params = {'v': v} - res = self.delete(self._url("/containers/" + container), params=params) + res = self.delete(self._url("/containers/" + container), params=params, + timeout=self._timeout) self._raise_for_status(res) def remove_image(self, image): - res = self.delete(self._url("/images/" + image)) + res = self.delete(self._url("/images/" + image), timeout=self._timeout) self._raise_for_status(res) def restart(self, container, timeout=10): @@ -463,12 +483,14 @@ class Client(requests.Session): container = container.get('Id') params = {'t': timeout} url = self._url("/containers/{0}/restart".format(container)) - res = self.post(url, None, params=params) + res = self.post(url, None, params=params, timeout=self._timeout) self._raise_for_status(res) def search(self, term): return self._result(self.get(self._url("/images/search"), - params={'term': term}), True) + params={'term': term}, + timeout=self._timeout), + True) def start(self, container, binds=None, port_bindings=None, lxc_conf=None): if isinstance(container, dict): @@ -501,7 +523,7 @@ class Client(requests.Session): container = container.get('Id') params = {'t': timeout} url = self._url("/containers/{0}/stop".format(container)) - res = self.post(url, None, params=params) + res = self.post(url, None, params=params, timeout=self._timeout) self._raise_for_status(res) def tag(self, image, repository, tag=None, force=False): @@ -511,16 +533,18 @@ class Client(requests.Session): 'force': 1 if force else 0 } url = self._url("/images/{0}/tag".format(image)) - res = self.post(url, None, params=params) + res = self.post(url, None, params=params, timeout=self._timeout) self._raise_for_status(res) return res.status_code == 201 def top(self, container): u = self._url("/containers/{0}/top".format(container)) - return self._result(self.get(u), True) + return self._result(self.get(u, timeout=self._timeout), True) def version(self): - return self._result(self.get(self._url("/version")), True) + return self._result(self.get(self._url("/version"), + timeout=self._timeout), + True) def wait(self, container): if isinstance(container, dict): diff --git a/docker/unixconn/unixconn.py b/docker/unixconn/unixconn.py index 08910998..87a18999 100644 --- a/docker/unixconn/unixconn.py +++ b/docker/unixconn/unixconn.py @@ -25,15 +25,16 @@ try: except ImportError: import urllib3.connectionpool as connectionpool - class UnixHTTPConnection(httplib.HTTPConnection, object): - def __init__(self, base_url, unix_socket): - httplib.HTTPConnection.__init__(self, 'localhost') + def __init__(self, base_url, unix_socket, timeout=60): + httplib.HTTPConnection.__init__(self, 'localhost', timeout=timeout) self.base_url = base_url self.unix_socket = unix_socket + self.timeout = timeout def connect(self): sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + sock.settimeout(self.timeout) sock.connect(self.base_url.replace("unix:/", "")) self.sock = sock @@ -47,19 +48,22 @@ class UnixHTTPConnection(httplib.HTTPConnection, object): class UnixHTTPConnectionPool(connectionpool.HTTPConnectionPool): - def __init__(self, base_url, socket_path): - self.socket_path = socket_path + def __init__(self, base_url, socket_path, timeout=60): + connectionpool.HTTPConnectionPool.__init__(self, 'localhost', + timeout=timeout) self.base_url = base_url - super(UnixHTTPConnectionPool, self).__init__(self, 'localhost') + self.socket_path = socket_path + self.timeout = timeout def _new_conn(self): - return UnixHTTPConnection(self.base_url, self.socket_path) + return UnixHTTPConnection(self.base_url, self.socket_path, self.timeout) class UnixAdapter(requests.adapters.HTTPAdapter): - def __init__(self, base_url): + def __init__(self, base_url, timeout=60): self.base_url = base_url + self.timeout = timeout super(UnixAdapter, self).__init__() def get_connection(self, socket_path, proxies=None): - return UnixHTTPConnectionPool(self.base_url, socket_path) + return UnixHTTPConnectionPool(self.base_url, socket_path, self.timeout) diff --git a/tests/integration_test.py b/tests/integration_test.py index fd77c930..8816f362 100644 --- a/tests/integration_test.py +++ b/tests/integration_test.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +import time import base64 import io import os @@ -73,9 +74,9 @@ class TestSearch(BaseTestCase): def runTest(self): res = self.client.search('busybox') self.assertTrue(len(res) >= 1) - base_img = [x for x in res if x['Name'] == 'busybox'] + base_img = [x for x in res if x['name'] == 'busybox'] self.assertEqual(len(base_img), 1) - self.assertIn('Description', base_img[0]) + self.assertIn('description', base_img[0]) ################### ## LISTING TESTS ## @@ -165,7 +166,10 @@ class TestCreateContainerPrivileged(BaseTestCase): res = self.client.create_container('busybox', 'true', privileged=True) inspect = self.client.inspect_container(res['Id']) self.assertIn('Config', inspect) - self.assertEqual(inspect['Config']['Privileged'], True) + # Since Nov 2013, the Privileged flag is no longer part of the + # container's config exposed via the API (safety concerns?). + # + # self.assertEqual(inspect['Config']['Privileged'], True) class TestCreateContainerWithName(BaseTestCase): @@ -602,5 +606,23 @@ class TestLoadConfig(BaseTestCase): self.assertEqual(cfg['Email'], 'sakuya@scarlet.net') self.assertEqual(cfg.get('Auth'), None) + +class TestConnectionTimeout(unittest.TestCase): + def setUp(self): + self.timeout = 0.5 + self.client = docker.client.Client(base_url='http://192.168.10.2:4243', + timeout=self.timeout) + + def runTest(self): + start = time.time() + res = None + # This call isn't supposed to complete, and it should fail fast. + try: res = self.client.inspect_container('id') + except: pass + end = time.time() + self.assertTrue(res is None) + self.assertTrue(end - start < 2 * self.timeout) + + if __name__ == '__main__': unittest.main() diff --git a/tests/test.py b/tests/test.py index dce465c1..02caf83d 100644 --- a/tests/test.py +++ b/tests/test.py @@ -76,7 +76,8 @@ class DockerClientTest(unittest.TestCase): self.fail('Command should not raise exception: {0}'.format(e)) fake_request.assert_called_with( - 'unix://var/run/docker.sock/v1.4/version' + 'unix://var/run/docker.sock/v1.4/version', + timeout=docker.client.DEFAULT_TIMEOUT_SECONDS ) def test_info(self): @@ -85,7 +86,8 @@ class DockerClientTest(unittest.TestCase): except Exception as e: self.fail('Command should not raise exception: {0}'.format(e)) - fake_request.assert_called_with('unix://var/run/docker.sock/v1.4/info') + fake_request.assert_called_with('unix://var/run/docker.sock/v1.4/info', + timeout=docker.client.DEFAULT_TIMEOUT_SECONDS) def test_search(self): try: @@ -95,7 +97,8 @@ class DockerClientTest(unittest.TestCase): fake_request.assert_called_with( 'unix://var/run/docker.sock/v1.4/images/search', - params={'term': 'busybox'} + params={'term': 'busybox'}, + timeout=docker.client.DEFAULT_TIMEOUT_SECONDS ) ################### @@ -109,7 +112,8 @@ class DockerClientTest(unittest.TestCase): self.fail('Command should not raise exception: {0}'.format(e)) fake_request.assert_called_with( 'unix://var/run/docker.sock/v1.4/images/json', - params={'filter': None, 'only_ids': 0, 'all': 1} + params={'filter': None, 'only_ids': 0, 'all': 1}, + timeout=docker.client.DEFAULT_TIMEOUT_SECONDS ) def test_image_ids(self): @@ -120,7 +124,8 @@ class DockerClientTest(unittest.TestCase): fake_request.assert_called_with( 'unix://var/run/docker.sock/v1.4/images/json', - params={'filter': None, 'only_ids': 1, 'all': 0} + params={'filter': None, 'only_ids': 1, 'all': 0}, + timeout=docker.client.DEFAULT_TIMEOUT_SECONDS ) def test_list_containers(self): @@ -137,7 +142,8 @@ class DockerClientTest(unittest.TestCase): 'limit': -1, 'trunc_cmd': 1, 'before': None - } + }, + timeout=docker.client.DEFAULT_TIMEOUT_SECONDS ) ##################### @@ -232,7 +238,8 @@ class DockerClientTest(unittest.TestCase): fake_request.assert_called_with( 'unix://var/run/docker.sock/v1.4/containers/3cc2351ab11b/start', '{}', - headers={'Content-Type': 'application/json'} + headers={'Content-Type': 'application/json'}, + timeout=docker.client.DEFAULT_TIMEOUT_SECONDS ) def test_start_container_with_lxc_conf(self): @@ -283,7 +290,8 @@ class DockerClientTest(unittest.TestCase): fake_request.assert_called_with( 'unix://var/run/docker.sock/v1.4/containers/3cc2351ab11b/start', '{"Binds": ["/tmp:/mnt"]}', - headers={'Content-Type': 'application/json'} + headers={'Content-Type': 'application/json'}, + timeout=docker.client.DEFAULT_TIMEOUT_SECONDS ) def test_start_container_with_dict_instead_of_id(self): @@ -293,7 +301,8 @@ class DockerClientTest(unittest.TestCase): self.fail('Command should not raise exception: {0}'.format(e)) fake_request.assert_called_with( 'unix://var/run/docker.sock/v1.4/containers/3cc2351ab11b/start', - '{}', headers={'Content-Type': 'application/json'} + '{}', headers={'Content-Type': 'application/json'}, + timeout=docker.client.DEFAULT_TIMEOUT_SECONDS ) def test_wait(self): @@ -329,7 +338,8 @@ class DockerClientTest(unittest.TestCase): fake_request.assert_called_with( 'unix://var/run/docker.sock/v1.4/containers/3cc2351ab11b/attach', None, - params={'logs': 1, 'stderr': 1, 'stdout': 1} + params={'logs': 1, 'stderr': 1, 'stdout': 1}, + timeout=docker.client.DEFAULT_TIMEOUT_SECONDS ) def test_logs_with_dict_instead_of_id(self): @@ -341,7 +351,8 @@ class DockerClientTest(unittest.TestCase): fake_request.assert_called_with( 'unix://var/run/docker.sock/v1.4/containers/3cc2351ab11b/attach', None, - params={'logs': 1, 'stderr': 1, 'stdout': 1} + params={'logs': 1, 'stderr': 1, 'stdout': 1}, + timeout=docker.client.DEFAULT_TIMEOUT_SECONDS ) def test_diff(self): @@ -351,7 +362,8 @@ class DockerClientTest(unittest.TestCase): self.fail('Command should not raise exception: {0}'.format(e)) fake_request.assert_called_with( - 'unix://var/run/docker.sock/v1.4/containers/3cc2351ab11b/changes') + 'unix://var/run/docker.sock/v1.4/containers/3cc2351ab11b/changes', + timeout=docker.client.DEFAULT_TIMEOUT_SECONDS) def test_diff_with_dict_instead_of_id(self): try: @@ -360,7 +372,8 @@ class DockerClientTest(unittest.TestCase): self.fail('Command should not raise exception: {0}'.format(e)) fake_request.assert_called_with( - 'unix://var/run/docker.sock/v1.4/containers/3cc2351ab11b/changes') + 'unix://var/run/docker.sock/v1.4/containers/3cc2351ab11b/changes', + timeout=docker.client.DEFAULT_TIMEOUT_SECONDS) def test_stop_container(self): try: @@ -371,7 +384,8 @@ class DockerClientTest(unittest.TestCase): fake_request.assert_called_with( 'unix://var/run/docker.sock/v1.4/containers/3cc2351ab11b/stop', None, - params={'t': 2} + params={'t': 2}, + timeout=docker.client.DEFAULT_TIMEOUT_SECONDS ) def test_stop_container_with_dict_instead_of_id(self): @@ -383,7 +397,8 @@ class DockerClientTest(unittest.TestCase): fake_request.assert_called_with( 'unix://var/run/docker.sock/v1.4/containers/3cc2351ab11b/stop', None, - params={'t': 2} + params={'t': 2}, + timeout=docker.client.DEFAULT_TIMEOUT_SECONDS ) def test_kill_container(self): @@ -394,7 +409,8 @@ class DockerClientTest(unittest.TestCase): fake_request.assert_called_with( 'unix://var/run/docker.sock/v1.4/containers/3cc2351ab11b/kill', - None + None, + timeout=docker.client.DEFAULT_TIMEOUT_SECONDS ) def test_kill_container_with_dict_instead_of_id(self): @@ -405,7 +421,8 @@ class DockerClientTest(unittest.TestCase): fake_request.assert_called_with( 'unix://var/run/docker.sock/v1.4/containers/3cc2351ab11b/kill', - None + None, + timeout=docker.client.DEFAULT_TIMEOUT_SECONDS ) def test_restart_container(self): @@ -417,7 +434,8 @@ class DockerClientTest(unittest.TestCase): fake_request.assert_called_with( 'unix://var/run/docker.sock/v1.4/containers/3cc2351ab11b/restart', None, - params={'t': 2} + params={'t': 2}, + timeout=docker.client.DEFAULT_TIMEOUT_SECONDS ) def test_restart_container_with_dict_instead_of_id(self): @@ -429,7 +447,8 @@ class DockerClientTest(unittest.TestCase): fake_request.assert_called_with( 'unix://var/run/docker.sock/v1.4/containers/3cc2351ab11b/restart', None, - params={'t': 2} + params={'t': 2}, + timeout=docker.client.DEFAULT_TIMEOUT_SECONDS ) def test_remove_container(self): @@ -440,7 +459,8 @@ class DockerClientTest(unittest.TestCase): fake_request.assert_called_with( 'unix://var/run/docker.sock/v1.4/containers/3cc2351ab11b', - params={'v': False} + params={'v': False}, + timeout=docker.client.DEFAULT_TIMEOUT_SECONDS ) def test_remove_container_with_dict_instead_of_id(self): @@ -451,7 +471,8 @@ class DockerClientTest(unittest.TestCase): fake_request.assert_called_with( 'unix://var/run/docker.sock/v1.4/containers/3cc2351ab11b', - params={'v': False} + params={'v': False}, + timeout=docker.client.DEFAULT_TIMEOUT_SECONDS ) ################## @@ -467,7 +488,8 @@ class DockerClientTest(unittest.TestCase): fake_request.assert_called_with( 'unix://var/run/docker.sock/v1.4/images/create', headers={}, - params={'tag': None, 'fromImage': 'joffrey/test001'} + params={'tag': None, 'fromImage': 'joffrey/test001'}, + timeout=docker.client.DEFAULT_TIMEOUT_SECONDS ) def test_commit(self): @@ -486,7 +508,8 @@ class DockerClientTest(unittest.TestCase): 'tag': None, 'container': '3cc2351ab11b', 'author': None - } + }, + timeout=docker.client.DEFAULT_TIMEOUT_SECONDS ) def test_remove_image(self): @@ -496,7 +519,8 @@ class DockerClientTest(unittest.TestCase): self.fail('Command should not raise exception: {0}'.format(e)) fake_request.assert_called_with( - 'unix://var/run/docker.sock/v1.4/images/e9aa60c60128' + 'unix://var/run/docker.sock/v1.4/images/e9aa60c60128', + timeout=docker.client.DEFAULT_TIMEOUT_SECONDS ) #################