|
|
@@ -16,6 +16,7 @@ import json
|
|
|
import re
|
|
|
import shlex
|
|
|
import struct
|
|
|
+import warnings
|
|
|
|
|
|
import requests
|
|
|
import requests.exceptions
|
|
|
@@ -29,7 +30,7 @@ from . import errors
|
|
|
if not six.PY3:
|
|
|
import websocket
|
|
|
|
|
|
-DEFAULT_DOCKER_API_VERSION = '1.9'
|
|
|
+DEFAULT_DOCKER_API_VERSION = '1.12'
|
|
|
DEFAULT_TIMEOUT_SECONDS = 60
|
|
|
STREAM_HEADER_SIZE_BYTES = 8
|
|
|
|
|
|
@@ -95,7 +96,8 @@ class Client(requests.Session):
|
|
|
mem_limit=0, ports=None, environment=None, dns=None,
|
|
|
volumes=None, volumes_from=None,
|
|
|
network_disabled=False, entrypoint=None,
|
|
|
- cpu_shares=None, working_dir=None, domainname=None):
|
|
|
+ cpu_shares=None, working_dir=None, domainname=None,
|
|
|
+ memswap_limit=0):
|
|
|
if isinstance(command, six.string_types):
|
|
|
command = shlex.split(str(command))
|
|
|
if isinstance(environment, dict):
|
|
|
@@ -121,8 +123,12 @@ class Client(requests.Session):
|
|
|
volumes_dict[vol] = {}
|
|
|
volumes = volumes_dict
|
|
|
|
|
|
- if volumes_from and not isinstance(volumes_from, six.string_types):
|
|
|
- volumes_from = ','.join(volumes_from)
|
|
|
+ if volumes_from:
|
|
|
+ if not isinstance(volumes_from, six.string_types):
|
|
|
+ volumes_from = ','.join(volumes_from)
|
|
|
+ else:
|
|
|
+ # Force None, an empty list or dict causes client.start to fail
|
|
|
+ volumes_from = None
|
|
|
|
|
|
attach_stdin = False
|
|
|
attach_stdout = False
|
|
|
@@ -137,6 +143,14 @@ class Client(requests.Session):
|
|
|
attach_stdin = True
|
|
|
stdin_once = True
|
|
|
|
|
|
+ if utils.compare_version('1.10', self._version) >= 0:
|
|
|
+ message = ('{0!r} parameter has no effect on create_container().'
|
|
|
+ ' It has been moved to start()')
|
|
|
+ if dns is not None:
|
|
|
+ raise errors.DockerException(message.format('dns'))
|
|
|
+ if volumes_from is not None:
|
|
|
+ raise errors.DockerException(message.format('volumes_from'))
|
|
|
+
|
|
|
return {
|
|
|
'Hostname': hostname,
|
|
|
'Domainname': domainname,
|
|
|
@@ -158,7 +172,8 @@ class Client(requests.Session):
|
|
|
'NetworkDisabled': network_disabled,
|
|
|
'Entrypoint': entrypoint,
|
|
|
'CpuShares': cpu_shares,
|
|
|
- 'WorkingDir': working_dir
|
|
|
+ 'WorkingDir': working_dir,
|
|
|
+ 'MemorySwap': memswap_limit
|
|
|
}
|
|
|
|
|
|
def _post_json(self, url, data, **kwargs):
|
|
|
@@ -235,7 +250,7 @@ class Client(requests.Session):
|
|
|
start = walker + STREAM_HEADER_SIZE_BYTES
|
|
|
end = start + length
|
|
|
walker = end
|
|
|
- yield str(buf[start:end])
|
|
|
+ yield buf[start:end]
|
|
|
|
|
|
def _multiplexed_socket_stream_helper(self, response):
|
|
|
"""A generator of multiplexed data blocks coming from a response
|
|
|
@@ -296,8 +311,10 @@ class Client(requests.Session):
|
|
|
return stream_result() if stream else \
|
|
|
self._result(response, binary=True)
|
|
|
|
|
|
+ sep = bytes() if six.PY3 else str()
|
|
|
+
|
|
|
return stream and self._multiplexed_socket_stream_helper(response) or \
|
|
|
- ''.join([x for x in self._multiplexed_buffer_helper(response)])
|
|
|
+ sep.join([x for x in self._multiplexed_buffer_helper(response)])
|
|
|
|
|
|
def attach_socket(self, container, params=None, ws=False):
|
|
|
if params is None:
|
|
|
@@ -318,14 +335,20 @@ class Client(requests.Session):
|
|
|
u, None, params=self._attach_params(params), stream=True))
|
|
|
|
|
|
def build(self, path=None, tag=None, quiet=False, fileobj=None,
|
|
|
- nocache=False, rm=False, stream=False, timeout=None):
|
|
|
+ nocache=False, rm=False, stream=False, timeout=None,
|
|
|
+ custom_context=False, encoding=None):
|
|
|
remote = context = headers = None
|
|
|
if path is None and fileobj is None:
|
|
|
raise TypeError("Either path or fileobj needs to be provided.")
|
|
|
|
|
|
- if fileobj is not None:
|
|
|
+ if custom_context:
|
|
|
+ if not fileobj:
|
|
|
+ raise TypeError("You must specify fileobj with custom_context")
|
|
|
+ context = fileobj
|
|
|
+ elif fileobj is not None:
|
|
|
context = utils.mkbuildcontext(fileobj)
|
|
|
- elif path.startswith(('http://', 'https://', 'git://', 'github.com/')):
|
|
|
+ elif path.startswith(('http://', 'https://',
|
|
|
+ 'git://', 'github.com/')):
|
|
|
remote = path
|
|
|
else:
|
|
|
context = utils.tar(path)
|
|
|
@@ -341,8 +364,11 @@ class Client(requests.Session):
|
|
|
'nocache': nocache,
|
|
|
'rm': rm
|
|
|
}
|
|
|
+
|
|
|
if context is not None:
|
|
|
headers = {'Content-Type': 'application/tar'}
|
|
|
+ if encoding:
|
|
|
+ headers['Content-Encoding'] = encoding
|
|
|
|
|
|
if utils.compare_version('1.9', self._version) >= 0:
|
|
|
# If we don't have any auth data so far, try reloading the config
|
|
|
@@ -393,10 +419,11 @@ class Client(requests.Session):
|
|
|
json=True)
|
|
|
|
|
|
def containers(self, quiet=False, all=False, trunc=True, latest=False,
|
|
|
- since=None, before=None, limit=-1):
|
|
|
+ since=None, before=None, limit=-1, size=False):
|
|
|
params = {
|
|
|
'limit': 1 if latest else limit,
|
|
|
'all': 1 if all else 0,
|
|
|
+ 'size': 1 if size else 0,
|
|
|
'trunc_cmd': 1 if trunc else 0,
|
|
|
'since': since,
|
|
|
'before': before
|
|
|
@@ -424,12 +451,13 @@ class Client(requests.Session):
|
|
|
mem_limit=0, ports=None, environment=None, dns=None,
|
|
|
volumes=None, volumes_from=None,
|
|
|
network_disabled=False, name=None, entrypoint=None,
|
|
|
- cpu_shares=None, working_dir=None, domainname=None):
|
|
|
+ cpu_shares=None, working_dir=None, domainname=None,
|
|
|
+ memswap_limit=0):
|
|
|
|
|
|
config = self._container_config(
|
|
|
image, command, hostname, user, detach, stdin_open, tty, mem_limit,
|
|
|
ports, environment, dns, volumes, volumes_from, network_disabled,
|
|
|
- entrypoint, cpu_shares, working_dir, domainname
|
|
|
+ entrypoint, cpu_shares, working_dir, domainname, memswap_limit
|
|
|
)
|
|
|
return self.create_container_from_config(config, name)
|
|
|
|
|
|
@@ -458,6 +486,12 @@ class Client(requests.Session):
|
|
|
self._raise_for_status(res)
|
|
|
return res.raw
|
|
|
|
|
|
+ def get_image(self, image):
|
|
|
+ res = self._get(self._url("/images/{0}/get".format(image)),
|
|
|
+ stream=True)
|
|
|
+ self._raise_for_status(res)
|
|
|
+ return res.raw
|
|
|
+
|
|
|
def history(self, image):
|
|
|
res = self._get(self._url("/images/{0}/history".format(image)))
|
|
|
self._raise_for_status(res)
|
|
|
@@ -513,6 +547,10 @@ class Client(requests.Session):
|
|
|
True)
|
|
|
|
|
|
def insert(self, image, url, path):
|
|
|
+ if utils.compare_version('1.12', self._version) >= 0:
|
|
|
+ raise errors.DeprecatedMethod(
|
|
|
+ 'insert is not available for API version >=1.12'
|
|
|
+ )
|
|
|
api_url = self._url("/images/" + image + "/insert")
|
|
|
params = {
|
|
|
'url': url,
|
|
|
@@ -544,6 +582,10 @@ class Client(requests.Session):
|
|
|
|
|
|
self._raise_for_status(res)
|
|
|
|
|
|
+ def load_image(self, data):
|
|
|
+ res = self._post(self._url("/images/load"), data=data)
|
|
|
+ self._raise_for_status(res)
|
|
|
+
|
|
|
def login(self, username, password=None, email=None, registry=None,
|
|
|
reauth=False):
|
|
|
# If we don't have any auth data so far, try reloading the config file
|
|
|
@@ -572,7 +614,27 @@ class Client(requests.Session):
|
|
|
self._auth_configs[registry] = req_data
|
|
|
return self._result(response, json=True)
|
|
|
|
|
|
- def logs(self, container, stdout=True, stderr=True, stream=False):
|
|
|
+ def logs(self, container, stdout=True, stderr=True, stream=False,
|
|
|
+ timestamps=False):
|
|
|
+ if isinstance(container, dict):
|
|
|
+ container = container.get('Id')
|
|
|
+ if utils.compare_version('1.11', self._version) >= 0:
|
|
|
+ params = {'stderr': stderr and 1 or 0,
|
|
|
+ 'stdout': stdout and 1 or 0,
|
|
|
+ 'timestamps': timestamps and 1 or 0,
|
|
|
+ 'follow': stream and 1 or 0}
|
|
|
+ url = self._url("/containers/{0}/logs".format(container))
|
|
|
+ res = self._get(url, params=params, stream=stream)
|
|
|
+ if stream:
|
|
|
+ return self._multiplexed_socket_stream_helper(res)
|
|
|
+ elif six.PY3:
|
|
|
+ return bytes().join(
|
|
|
+ [x for x in self._multiplexed_buffer_helper(res)]
|
|
|
+ )
|
|
|
+ else:
|
|
|
+ return str().join(
|
|
|
+ [x for x in self._multiplexed_buffer_helper(res)]
|
|
|
+ )
|
|
|
return self.attach(
|
|
|
container,
|
|
|
stdout=stdout,
|
|
|
@@ -581,6 +643,9 @@ class Client(requests.Session):
|
|
|
logs=True
|
|
|
)
|
|
|
|
|
|
+ def ping(self):
|
|
|
+ return self._result(self._get(self._url('/_ping')))
|
|
|
+
|
|
|
def port(self, container, private_port):
|
|
|
if isinstance(container, dict):
|
|
|
container = container.get('Id')
|
|
|
@@ -597,6 +662,8 @@ class Client(requests.Session):
|
|
|
return h_ports
|
|
|
|
|
|
def pull(self, repository, tag=None, stream=False):
|
|
|
+ if not tag:
|
|
|
+ repository, tag = utils.parse_repository_tag(repository)
|
|
|
registry, repo_name = auth.resolve_repository_name(repository)
|
|
|
if repo_name.count(":") == 1:
|
|
|
repository, tag = repository.rsplit(":", 1)
|
|
|
@@ -653,16 +720,17 @@ class Client(requests.Session):
|
|
|
return stream and self._stream_helper(response) \
|
|
|
or self._result(response)
|
|
|
|
|
|
- def remove_container(self, container, v=False, link=False):
|
|
|
+ def remove_container(self, container, v=False, link=False, force=False):
|
|
|
if isinstance(container, dict):
|
|
|
container = container.get('Id')
|
|
|
- params = {'v': v, 'link': link}
|
|
|
+ params = {'v': v, 'link': link, 'force': force}
|
|
|
res = self._delete(self._url("/containers/" + container),
|
|
|
params=params)
|
|
|
self._raise_for_status(res)
|
|
|
|
|
|
- def remove_image(self, image):
|
|
|
- res = self._delete(self._url("/images/" + image))
|
|
|
+ def remove_image(self, image, force=False, noprune=False):
|
|
|
+ params = {'force': force, 'noprune': noprune}
|
|
|
+ res = self._delete(self._url("/images/" + image), params=params)
|
|
|
self._raise_for_status(res)
|
|
|
|
|
|
def restart(self, container, timeout=10):
|
|
|
@@ -678,8 +746,9 @@ class Client(requests.Session):
|
|
|
params={'term': term}),
|
|
|
True)
|
|
|
|
|
|
- def start(self, container, binds=None, volumes_from=None, port_bindings=None,
|
|
|
- lxc_conf=None, publish_all_ports=False, links=None, privileged=False, network_mode=None):
|
|
|
+ def start(self, container, binds=None, port_bindings=None, lxc_conf=None,
|
|
|
+ publish_all_ports=False, links=None, privileged=False,
|
|
|
+ dns=None, dns_search=None, volumes_from=None, network_mode=None):
|
|
|
if isinstance(container, dict):
|
|
|
container = container.get('Id')
|
|
|
|
|
|
@@ -693,19 +762,7 @@ class Client(requests.Session):
|
|
|
'LxcConf': lxc_conf
|
|
|
}
|
|
|
if binds:
|
|
|
- bind_pairs = [
|
|
|
- '%s:%s:%s' % (
|
|
|
- h, d['bind'],
|
|
|
- 'ro' if 'ro' in d and d['ro'] else 'rw'
|
|
|
- ) for h, d in binds.items()
|
|
|
- ]
|
|
|
-
|
|
|
- start_config['Binds'] = bind_pairs
|
|
|
-
|
|
|
- if volumes_from and not isinstance(volumes_from, six.string_types):
|
|
|
- volumes_from = ','.join(volumes_from)
|
|
|
-
|
|
|
- start_config['VolumesFrom'] = volumes_from
|
|
|
+ start_config['Binds'] = utils.convert_volume_binds(binds)
|
|
|
|
|
|
if port_bindings:
|
|
|
start_config['PortBindings'] = utils.convert_port_bindings(
|
|
|
@@ -726,6 +783,28 @@ class Client(requests.Session):
|
|
|
|
|
|
start_config['Privileged'] = privileged
|
|
|
|
|
|
+ if utils.compare_version('1.10', self._version) >= 0:
|
|
|
+ if dns is not None:
|
|
|
+ start_config['Dns'] = dns
|
|
|
+ if volumes_from is not None:
|
|
|
+ if isinstance(volumes_from, six.string_types):
|
|
|
+ volumes_from = volumes_from.split(',')
|
|
|
+ start_config['VolumesFrom'] = volumes_from
|
|
|
+ else:
|
|
|
+ warning_message = ('{0!r} parameter is discarded. It is only'
|
|
|
+ ' available for API version greater or equal'
|
|
|
+ ' than 1.10')
|
|
|
+
|
|
|
+ if dns is not None:
|
|
|
+ warnings.warn(warning_message.format('dns'),
|
|
|
+ DeprecationWarning)
|
|
|
+ if volumes_from is not None:
|
|
|
+ warnings.warn(warning_message.format('volumes_from'),
|
|
|
+ DeprecationWarning)
|
|
|
+
|
|
|
+ if dns_search:
|
|
|
+ start_config['DnsSearch'] = dns_search
|
|
|
+
|
|
|
if network_mode:
|
|
|
start_config['NetworkMode'] = network_mode
|
|
|
|
|
|
@@ -733,6 +812,15 @@ class Client(requests.Session):
|
|
|
res = self._post_json(url, data=start_config)
|
|
|
self._raise_for_status(res)
|
|
|
|
|
|
+ def resize(self, container, height, width):
|
|
|
+ if isinstance(container, dict):
|
|
|
+ container = container.get('Id')
|
|
|
+
|
|
|
+ params = {'h': height, 'w': width}
|
|
|
+ url = self._url("/containers/{0}/resize".format(container))
|
|
|
+ res = self._post(url, params=params)
|
|
|
+ self._raise_for_status(res)
|
|
|
+
|
|
|
def stop(self, container, timeout=10):
|
|
|
if isinstance(container, dict):
|
|
|
container = container.get('Id')
|