diff options
author | Joffrey F <f.joffrey@gmail.com> | 2016-06-14 12:05:35 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2016-06-14 12:05:35 -0700 |
commit | 787f3f5a16999e54835aae884d16dbed3910d061 (patch) | |
tree | 3f71213b32bb7b886d988af74c163184f4ae98e3 | |
parent | 080b4711f23c0f429f0bb15a9e397c59f6685741 (diff) | |
parent | a8746f7a99907f8657698ac42afba41823066386 (diff) | |
download | docker-py-787f3f5a16999e54835aae884d16dbed3910d061.tar.gz |
Merge pull request #1079 from docker/1024-npipe-support
npipe support
-rw-r--r-- | docker/client.py | 28 | ||||
-rw-r--r-- | docker/constants.py | 4 | ||||
-rw-r--r-- | docker/transport/__init__.py | 6 | ||||
-rw-r--r-- | docker/transport/npipeconn.py | 80 | ||||
-rw-r--r-- | docker/transport/npipesocket.py | 191 | ||||
-rw-r--r-- | docker/transport/unixconn.py (renamed from docker/unixconn/unixconn.py) | 0 | ||||
-rw-r--r-- | docker/unixconn/__init__.py | 1 | ||||
-rw-r--r-- | docker/utils/utils.py | 9 | ||||
-rw-r--r-- | setup.py | 8 | ||||
-rw-r--r-- | tests/unit/utils_test.py | 7 | ||||
-rw-r--r-- | win32-requirements.txt | 2 |
11 files changed, 322 insertions, 14 deletions
diff --git a/docker/client.py b/docker/client.py index de3cb3c..b96a78c 100644 --- a/docker/client.py +++ b/docker/client.py @@ -14,7 +14,6 @@ import json import struct -import sys import requests import requests.exceptions @@ -26,10 +25,14 @@ from . import api from . import constants from . import errors from .auth import auth -from .unixconn import unixconn from .ssladapter import ssladapter -from .utils import utils, check_resource, update_headers, kwargs_from_env from .tls import TLSConfig +from .transport import UnixAdapter +from .utils import utils, check_resource, update_headers, kwargs_from_env +try: + from .transport import NpipeAdapter +except ImportError: + pass def from_env(**kwargs): @@ -59,11 +62,26 @@ class Client( self._auth_configs = auth.load_config() - base_url = utils.parse_host(base_url, sys.platform, tls=bool(tls)) + base_url = utils.parse_host( + base_url, constants.IS_WINDOWS_PLATFORM, tls=bool(tls) + ) if base_url.startswith('http+unix://'): - self._custom_adapter = unixconn.UnixAdapter(base_url, timeout) + self._custom_adapter = UnixAdapter(base_url, timeout) self.mount('http+docker://', self._custom_adapter) self.base_url = 'http+docker://localunixsocket' + elif base_url.startswith('npipe://'): + if not constants.IS_WINDOWS_PLATFORM: + raise errors.DockerException( + 'The npipe:// protocol is only supported on Windows' + ) + try: + self._custom_adapter = NpipeAdapter(base_url, timeout) + except NameError: + raise errors.DockerException( + 'Install pypiwin32 package to enable npipe:// support' + ) + self.mount('http+docker://', self._custom_adapter) + self.base_url = 'http+docker://localnpipe' else: # Use SSLAdapter for the ability to specify SSL version if isinstance(tls, TLSConfig): diff --git a/docker/constants.py b/docker/constants.py index 6c381de..0388f70 100644 --- a/docker/constants.py +++ b/docker/constants.py @@ -1,3 +1,5 @@ +import sys + DEFAULT_DOCKER_API_VERSION = '1.22' DEFAULT_TIMEOUT_SECONDS = 60 STREAM_HEADER_SIZE_BYTES = 8 @@ -8,3 +10,5 @@ CONTAINER_LIMITS_KEYS = [ INSECURE_REGISTRY_DEPRECATION_WARNING = \ 'The `insecure_registry` argument to {} ' \ 'is deprecated and non-functional. Please remove it.' + +IS_WINDOWS_PLATFORM = (sys.platform == 'win32') diff --git a/docker/transport/__init__.py b/docker/transport/__init__.py new file mode 100644 index 0000000..d647483 --- /dev/null +++ b/docker/transport/__init__.py @@ -0,0 +1,6 @@ +# flake8: noqa +from .unixconn import UnixAdapter +try: + from .npipeconn import NpipeAdapter +except ImportError: + pass
\ No newline at end of file diff --git a/docker/transport/npipeconn.py b/docker/transport/npipeconn.py new file mode 100644 index 0000000..736ddf6 --- /dev/null +++ b/docker/transport/npipeconn.py @@ -0,0 +1,80 @@ +import six +import requests.adapters + +from .npipesocket import NpipeSocket + +if six.PY3: + import http.client as httplib +else: + import httplib + +try: + import requests.packages.urllib3 as urllib3 +except ImportError: + import urllib3 + + +RecentlyUsedContainer = urllib3._collections.RecentlyUsedContainer + + +class NpipeHTTPConnection(httplib.HTTPConnection, object): + def __init__(self, npipe_path, timeout=60): + super(NpipeHTTPConnection, self).__init__( + 'localhost', timeout=timeout + ) + self.npipe_path = npipe_path + self.timeout = timeout + + def connect(self): + sock = NpipeSocket() + sock.settimeout(self.timeout) + sock.connect(self.npipe_path) + self.sock = sock + + +class NpipeHTTPConnectionPool(urllib3.connectionpool.HTTPConnectionPool): + def __init__(self, npipe_path, timeout=60): + super(NpipeHTTPConnectionPool, self).__init__( + 'localhost', timeout=timeout + ) + self.npipe_path = npipe_path + self.timeout = timeout + + def _new_conn(self): + return NpipeHTTPConnection( + self.npipe_path, self.timeout + ) + + +class NpipeAdapter(requests.adapters.HTTPAdapter): + def __init__(self, base_url, timeout=60): + self.npipe_path = base_url.replace('npipe://', '') + self.timeout = timeout + self.pools = RecentlyUsedContainer( + 10, dispose_func=lambda p: p.close() + ) + super(NpipeAdapter, self).__init__() + + def get_connection(self, url, proxies=None): + with self.pools.lock: + pool = self.pools.get(url) + if pool: + return pool + + pool = NpipeHTTPConnectionPool( + self.npipe_path, self.timeout + ) + self.pools[url] = pool + + return pool + + def request_url(self, request, proxies): + # The select_proxy utility in requests errors out when the provided URL + # doesn't have a hostname, like is the case when using a UNIX socket. + # Since proxies are an irrelevant notion in the case of UNIX sockets + # anyway, we simply return the path URL directly. + # See also: https://github.com/docker/docker-py/issues/811 + return request.path_url + + def close(self): + self.pools.clear() diff --git a/docker/transport/npipesocket.py b/docker/transport/npipesocket.py new file mode 100644 index 0000000..35418ef --- /dev/null +++ b/docker/transport/npipesocket.py @@ -0,0 +1,191 @@ +import functools +import io + +import win32file +import win32pipe + +cSECURITY_SQOS_PRESENT = 0x100000 +cSECURITY_ANONYMOUS = 0 +cPIPE_READMODE_MESSAGE = 2 + + +def check_closed(f): + @functools.wraps(f) + def wrapped(self, *args, **kwargs): + if self._closed: + raise RuntimeError( + 'Can not reuse socket after connection was closed.' + ) + return f(self, *args, **kwargs) + return wrapped + + +class NpipeSocket(object): + """ Partial implementation of the socket API over windows named pipes. + This implementation is only designed to be used as a client socket, + and server-specific methods (bind, listen, accept...) are not + implemented. + """ + def __init__(self, handle=None): + self._timeout = win32pipe.NMPWAIT_USE_DEFAULT_WAIT + self._handle = handle + self._closed = False + + def accept(self): + raise NotImplementedError() + + def bind(self, address): + raise NotImplementedError() + + def close(self): + self._handle.Close() + self._closed = True + + @check_closed + def connect(self, address): + win32pipe.WaitNamedPipe(address, self._timeout) + handle = win32file.CreateFile( + address, + win32file.GENERIC_READ | win32file.GENERIC_WRITE, + 0, + None, + win32file.OPEN_EXISTING, + cSECURITY_ANONYMOUS | cSECURITY_SQOS_PRESENT, + 0 + ) + self.flags = win32pipe.GetNamedPipeInfo(handle)[0] + + self._handle = handle + self._address = address + + @check_closed + def connect_ex(self, address): + return self.connect(address) + + @check_closed + def detach(self): + self._closed = True + return self._handle + + @check_closed + def dup(self): + return NpipeSocket(self._handle) + + @check_closed + def fileno(self): + return int(self._handle) + + def getpeername(self): + return self._address + + def getsockname(self): + return self._address + + def getsockopt(self, level, optname, buflen=None): + raise NotImplementedError() + + def ioctl(self, control, option): + raise NotImplementedError() + + def listen(self, backlog): + raise NotImplementedError() + + def makefile(self, mode=None, bufsize=None): + if mode.strip('b') != 'r': + raise NotImplementedError() + rawio = NpipeFileIOBase(self) + if bufsize is None: + bufsize = io.DEFAULT_BUFFER_SIZE + return io.BufferedReader(rawio, buffer_size=bufsize) + + @check_closed + def recv(self, bufsize, flags=0): + err, data = win32file.ReadFile(self._handle, bufsize) + return data + + @check_closed + def recvfrom(self, bufsize, flags=0): + data = self.recv(bufsize, flags) + return (data, self._address) + + @check_closed + def recvfrom_into(self, buf, nbytes=0, flags=0): + return self.recv_into(buf, nbytes, flags), self._address + + @check_closed + def recv_into(self, buf, nbytes=0): + readbuf = buf + if not isinstance(buf, memoryview): + readbuf = memoryview(buf) + + err, data = win32file.ReadFile( + self._handle, + readbuf[:nbytes] if nbytes else readbuf + ) + return len(data) + + @check_closed + def send(self, string, flags=0): + err, nbytes = win32file.WriteFile(self._handle, string) + return nbytes + + @check_closed + def sendall(self, string, flags=0): + return self.send(string, flags) + + @check_closed + def sendto(self, string, address): + self.connect(address) + return self.send(string) + + def setblocking(self, flag): + if flag: + return self.settimeout(None) + return self.settimeout(0) + + def settimeout(self, value): + if value is None: + self._timeout = win32pipe.NMPWAIT_NOWAIT + elif not isinstance(value, (float, int)) or value < 0: + raise ValueError('Timeout value out of range') + elif value == 0: + self._timeout = win32pipe.NMPWAIT_USE_DEFAULT_WAIT + else: + self._timeout = value + + def gettimeout(self): + return self._timeout + + def setsockopt(self, level, optname, value): + raise NotImplementedError() + + @check_closed + def shutdown(self, how): + return self.close() + + +class NpipeFileIOBase(io.RawIOBase): + def __init__(self, npipe_socket): + self.sock = npipe_socket + + def close(self): + super(NpipeFileIOBase, self).close() + self.sock = None + + def fileno(self): + return self.sock.fileno() + + def isatty(self): + return False + + def readable(self): + return True + + def readinto(self, buf): + return self.sock.recv_into(buf) + + def seekable(self): + return False + + def writable(self): + return False diff --git a/docker/unixconn/unixconn.py b/docker/transport/unixconn.py index f4d83ef..f4d83ef 100644 --- a/docker/unixconn/unixconn.py +++ b/docker/transport/unixconn.py diff --git a/docker/unixconn/__init__.py b/docker/unixconn/__init__.py deleted file mode 100644 index 53711fc..0000000 --- a/docker/unixconn/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .unixconn import UnixAdapter # flake8: noqa diff --git a/docker/utils/utils.py b/docker/utils/utils.py index ee48bba..f234615 100644 --- a/docker/utils/utils.py +++ b/docker/utils/utils.py @@ -383,13 +383,13 @@ def parse_repository_tag(repo_name): # fd:// protocol unsupported (for obvious reasons) # Added support for http and https # Protocol translation: tcp -> http, unix -> http+unix -def parse_host(addr, platform=None, tls=False): +def parse_host(addr, is_win32=False, tls=False): proto = "http+unix" host = DEFAULT_HTTP_HOST port = None path = '' - if not addr and platform == 'win32': + if not addr and is_win32: addr = '{0}:{1}'.format(DEFAULT_HTTP_HOST, 2375) if not addr or addr.strip() == 'unix://': @@ -413,6 +413,9 @@ def parse_host(addr, platform=None, tls=False): elif addr.startswith('https://'): proto = "https" addr = addr[8:] + elif addr.startswith('npipe://'): + proto = 'npipe' + addr = addr[8:] elif addr.startswith('fd://'): raise errors.DockerException("fd protocol is not implemented") else: @@ -448,7 +451,7 @@ def parse_host(addr, platform=None, tls=False): else: host = addr - if proto == "http+unix": + if proto == "http+unix" or proto == 'npipe': return "{0}://{1}".format(proto, host) return "{0}://{1}:{2}{3}".format(proto, host, port, path) @@ -1,7 +1,10 @@ #!/usr/bin/env python import os +import sys + from setuptools import setup + ROOT_DIR = os.path.dirname(__file__) SOURCE_DIR = os.path.join(ROOT_DIR) @@ -11,6 +14,9 @@ requirements = [ 'websocket-client >= 0.32.0', ] +if sys.platform == 'win32': + requirements.append('pypiwin32 >= 219') + extras_require = { ':python_version < "3.5"': 'backports.ssl_match_hostname >= 3.5', ':python_version < "3.3"': 'ipaddress >= 1.0.16', @@ -29,7 +35,7 @@ setup( description="Python client for Docker.", url='https://github.com/docker/docker-py/', packages=[ - 'docker', 'docker.api', 'docker.auth', 'docker.unixconn', + 'docker', 'docker.api', 'docker.auth', 'docker.transport', 'docker.utils', 'docker.utils.ports', 'docker.ssladapter' ], install_requires=requirements, diff --git a/tests/unit/utils_test.py b/tests/unit/utils_test.py index ef927d3..ae821fd 100644 --- a/tests/unit/utils_test.py +++ b/tests/unit/utils_test.py @@ -388,6 +388,7 @@ class ParseHostTest(base.BaseTestCase): 'somehost.net:80/service/swarm': ( 'http://somehost.net:80/service/swarm' ), + 'npipe:////./pipe/docker_engine': 'npipe:////./pipe/docker_engine', } for host in invalid_hosts: @@ -402,10 +403,8 @@ class ParseHostTest(base.BaseTestCase): tcp_port = 'http://127.0.0.1:2375' for val in [None, '']: - for platform in ['darwin', 'linux2', None]: - assert parse_host(val, platform) == unix_socket - - assert parse_host(val, 'win32') == tcp_port + assert parse_host(val, is_win32=False) == unix_socket + assert parse_host(val, is_win32=True) == tcp_port def test_parse_host_tls(self): host_value = 'myhost.docker.net:3348' diff --git a/win32-requirements.txt b/win32-requirements.txt new file mode 100644 index 0000000..e77c3d9 --- /dev/null +++ b/win32-requirements.txt @@ -0,0 +1,2 @@ +-r requirements.txt +pypiwin32==219
\ No newline at end of file |