# Copyright 2013 dotCloud inc. # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # http://www.apache.org/licenses/LICENSE-2.0 # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import json import os import re import shlex import struct import warnings from datetime import datetime import requests import requests.exceptions import six from . import constants from . import errors from .auth import auth from .unixconn import unixconn from .ssladapter import ssladapter from .utils import utils, check_resource from .tls import TLSConfig if not six.PY3: import websocket class Client(requests.Session): def __init__(self, base_url=None, version=None, timeout=constants.DEFAULT_TIMEOUT_SECONDS, tls=False): super(Client, self).__init__() if tls and not base_url.startswith('https://'): raise errors.TLSParameterError( 'If using TLS, the base_url argument must begin with ' '"https://".') self.base_url = base_url self.timeout = timeout self._auth_configs = auth.load_config() base_url = utils.parse_host(base_url) if base_url.startswith('http+unix://'): unix_socket_adapter = unixconn.UnixAdapter(base_url, timeout) self.mount('http+docker://', unix_socket_adapter) self.base_url = 'http+docker://localunixsocket' else: # Use SSLAdapter for the ability to specify SSL version if isinstance(tls, TLSConfig): tls.configure_client(self) elif tls: self.mount('https://', ssladapter.SSLAdapter()) self.base_url = base_url # version detection needs to be after unix adapter mounting if version is None: self._version = constants.DEFAULT_DOCKER_API_VERSION elif isinstance(version, six.string_types): if version.lower() == 'auto': self._version = self._retrieve_server_version() else: self._version = version else: raise errors.DockerException( 'Version parameter must be a string or None. Found {0}'.format( type(version).__name__ ) ) def _retrieve_server_version(self): try: return self.version(api_version=False)["ApiVersion"] except KeyError: raise errors.DockerException( 'Invalid response from docker daemon: key "ApiVersion"' ' is missing.' ) except Exception as e: raise errors.DockerException( 'Error while fetching server API version: {0}'.format(e) ) def _set_request_timeout(self, kwargs): """Prepare the kwargs for an HTTP request by inserting the timeout parameter, if not already present.""" kwargs.setdefault('timeout', self.timeout) return kwargs def _post(self, url, **kwargs): return self.post(url, **self._set_request_timeout(kwargs)) def _get(self, url, **kwargs): return self.get(url, **self._set_request_timeout(kwargs)) def _delete(self, url, **kwargs): return self.delete(url, **self._set_request_timeout(kwargs)) def _url(self, path, versioned_api=True): if versioned_api: return '{0}/v{1}{2}'.format(self.base_url, self._version, path) else: return '{0}{1}'.format(self.base_url, path) def _raise_for_status(self, response, explanation=None): """Raises stored :class:`APIError`, if one occurred.""" try: response.raise_for_status() except requests.exceptions.HTTPError as e: raise errors.APIError(e, response, explanation=explanation) def _result(self, response, json=False, binary=False): assert not (json and binary) self._raise_for_status(response) if json: return response.json() if binary: return response.content return response.text def _post_json(self, url, data, **kwargs): # Go <1.1 can't unserialize null to a string # so we do this disgusting thing here. data2 = {} if data is not None: for k, v in six.iteritems(data): if v is not None: data2[k] = v if 'headers' not in kwargs: kwargs['headers'] = {} kwargs['headers']['Content-Type'] = 'application/json' return self._post(url, data=json.dumps(data2), **kwargs) def _attach_params(self, override=None): return override or { 'stdout': 1, 'stderr': 1, 'stream': 1 } @check_resource def _attach_websocket(self, container, params=None): if six.PY3: raise NotImplementedError("This method is not currently supported " "under python 3") url = self._url("/containers/{0}/attach/ws".format(container)) req = requests.Request("POST", url, params=self._attach_params(params)) full_url = req.prepare().url full_url = full_url.replace("http://", "ws://", 1) full_url = full_url.replace("https://", "wss://", 1) return self._create_websocket_connection(full_url) def _create_websocket_connection(self, url): return websocket.create_connection(url) def _get_raw_response_socket(self, response): self._raise_for_status(response) if six.PY3: sock = response.raw._fp.fp.raw else: sock = response.raw._fp.fp._sock try: # Keep a reference to the response to stop it being garbage # collected. If the response is garbage collected, it will # close TLS sockets. sock._response = response except AttributeError: # UNIX sockets can't have attributes set on them, but that's # fine because we won't be doing TLS over them pass return sock def _stream_helper(self, response, decode=False): """Generator for data coming from a chunked-encoded HTTP response.""" if response.raw._fp.chunked: reader = response.raw while not reader.closed: # this read call will block until we get a chunk data = reader.read(1) if not data: break if reader._fp.chunk_left: data += reader.read(reader._fp.chunk_left) if decode: if six.PY3: data = data.decode('utf-8') data = json.loads(data) yield data else: # Response isn't chunked, meaning we probably # encountered an error immediately yield self._result(response) def _multiplexed_buffer_helper(self, response): """A generator of multiplexed data blocks read from a buffered response.""" buf = self._result(response, binary=True) walker = 0 while True: if len(buf[walker:]) < 8: break _, length = struct.unpack_from('>BxxxL', buf[walker:]) start = walker + constants.STREAM_HEADER_SIZE_BYTES end = start + length walker = end yield buf[start:end] def _multiplexed_response_stream_helper(self, response): """A generator of multiplexed data blocks coming from a response stream.""" # Disable timeout on the underlying socket to prevent # Read timed out(s) for long running processes socket = self._get_raw_response_socket(response) if six.PY3: socket._sock.settimeout(None) else: socket.settimeout(None) while True: header = response.raw.read(constants.STREAM_HEADER_SIZE_BYTES) if not header: break _, length = struct.unpack('>BxxxL', header) if not length: break data = response.raw.read(length) if not data: break yield data @property def api_version(self): return self._version @check_resource def attach(self, container, stdout=True, stderr=True, stream=False, logs=False): if isinstance(container, dict): container = container.get('Id') params = { 'logs': logs and 1 or 0, 'stdout': stdout and 1 or 0, 'stderr': stderr and 1 or 0, 'stream': stream and 1 or 0, } u = self._url("/containers/{0}/attach".format(container)) response = self._post(u, params=params, stream=stream) # Stream multi-plexing was only introduced in API v1.6. Anything before # that needs old-style streaming. if utils.compare_version('1.6', self._version) < 0: def stream_result(): self._raise_for_status(response) for line in response.iter_lines(chunk_size=1, decode_unicode=True): # filter out keep-alive new lines if line: yield line return stream_result() if stream else \ self._result(response, binary=True) sep = bytes() if six.PY3 else str() if stream: return self._multiplexed_response_stream_helper(response) else: return sep.join( [x for x in self._multiplexed_buffer_helper(response)] ) @check_resource def attach_socket(self, container, params=None, ws=False): if params is None: params = { 'stdout': 1, 'stderr': 1, 'stream': 1 } if ws: return self._attach_websocket(container, params) if isinstance(container, dict): container = container.get('Id') u = self._url("/containers/{0}/attach".format(container)) return self._get_raw_response_socket(self.post( 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, custom_context=False, encoding=None, pull=True, forcerm=False, dockerfile=None, container_limits=None, decode=False): remote = context = headers = None container_limits = container_limits or {} if path is None and fileobj is None: raise TypeError("Either path or fileobj needs to be provided.") for key in container_limits.keys(): if key not in constants.CONTAINER_LIMITS_KEYS: raise errors.DockerException( 'Invalid container_limits key {0}'.format(key) ) 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/')): remote = path elif not os.path.isdir(path): raise TypeError("You must specify a directory to build in path") else: dockerignore = os.path.join(path, '.dockerignore') exclude = None if os.path.exists(dockerignore): with open(dockerignore, 'r') as f: exclude = list(filter(bool, f.read().splitlines())) # These are handled by the docker daemon and should not be # excluded on the client if 'Dockerfile' in exclude: exclude.remove('Dockerfile') if '.dockerignore' in exclude: exclude.remove(".dockerignore") context = utils.tar(path, exclude=exclude) if utils.compare_version('1.8', self._version) >= 0: stream = True if dockerfile and utils.compare_version('1.17', self._version) < 0: raise errors.InvalidVersion( 'dockerfile was only introduced in API version 1.17' ) u = self._url('/build') params = { 't': tag, 'remote': remote, 'q': quiet, 'nocache': nocache, 'rm': rm, 'forcerm': forcerm, 'pull': pull, 'dockerfile': dockerfile, } params.update(container_limits) 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 # file one more time in case anything showed up in there. if not self._auth_configs: self._auth_configs = auth.load_config() # Send the full auth configuration (if any exists), since the build # could use any (or all) of the registries. if self._auth_configs: if headers is None: headers = {} headers['X-Registry-Config'] = auth.encode_full_header( self._auth_configs ) response = self._post( u, data=context, params=params, headers=headers, stream=stream, timeout=timeout, ) if context is not None and not custom_context: context.close() if stream: return self._stream_helper(response, decode=decode) else: output = self._result(response) srch = r'Successfully built ([0-9a-f]+)' match = re.search(srch, output) if not match: return None, output return match.group(1), output @check_resource def commit(self, container, repository=None, tag=None, message=None, author=None, conf=None): if isinstance(container, dict): container = container.get('Id') params = { 'container': container, 'repo': repository, 'tag': tag, 'comment': message, 'author': author } u = self._url("/commit") return self._result(self._post_json(u, data=conf, params=params), json=True) def containers(self, quiet=False, all=False, trunc=False, latest=False, since=None, before=None, limit=-1, size=False, filters=None): 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 } if filters: params['filters'] = utils.convert_filters(filters) u = self._url("/containers/json") res = self._result(self._get(u, params=params), True) if quiet: return [{'Id': x['Id']} for x in res] if trunc: for x in res: x['Id'] = x['Id'][:12] return res @check_resource def copy(self, container, resource): if isinstance(container, dict): container = container.get('Id') res = self._post_json( self._url("/containers/{0}/copy".format(container)), data={"Resource": resource}, stream=True ) self._raise_for_status(res) return res.raw def create_container(self, image, command=None, hostname=None, user=None, detach=False, stdin_open=False, tty=False, 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, memswap_limit=0, cpuset=None, host_config=None, mac_address=None, labels=None, volume_driver=None): if isinstance(volumes, six.string_types): volumes = [volumes, ] if host_config and utils.compare_version('1.15', self._version) < 0: raise errors.InvalidVersion( 'host_config is not supported in API < 1.15' ) config = utils.create_container_config( self._version, image, command, hostname, user, detach, stdin_open, tty, mem_limit, ports, environment, dns, volumes, volumes_from, network_disabled, entrypoint, cpu_shares, working_dir, domainname, memswap_limit, cpuset, host_config, mac_address, labels, volume_driver ) return self.create_container_from_config(config, name) def create_container_from_config(self, config, name=None): u = self._url("/containers/create") params = { 'name': name } res = self._post_json(u, data=config, params=params) return self._result(res, True) @check_resource def diff(self, container): if isinstance(container, dict): container = container.get('Id') return self._result(self._get(self._url("/containers/{0}/changes". format(container))), True) def events(self, since=None, until=None, filters=None, decode=None): if isinstance(since, datetime): since = utils.datetime_to_timestamp(since) if isinstance(until, datetime): until = utils.datetime_to_timestamp(until) if filters: filters = utils.convert_filters(filters) params = { 'since': since, 'until': until, 'filters': filters } return self._stream_helper(self.get(self._url('/events'), params=params, stream=True), decode=decode) @check_resource def execute(self, container, cmd, detach=False, stdout=True, stderr=True, stream=False, tty=False): warnings.warn( 'Client.execute is being deprecated. Please use exec_create & ' 'exec_start instead', DeprecationWarning ) create_res = self.exec_create( container, cmd, stdout, stderr, tty ) return self.exec_start(create_res, detach, tty, stream) def exec_create(self, container, cmd, stdout=True, stderr=True, tty=False, privileged=False): if utils.compare_version('1.15', self._version) < 0: raise errors.InvalidVersion('Exec is not supported in API < 1.15') if privileged and utils.compare_version('1.19', self._version) < 0: raise errors.InvalidVersion( 'Privileged exec is not supported in API < 1.19' ) if isinstance(container, dict): container = container.get('Id') if isinstance(cmd, six.string_types): cmd = shlex.split(str(cmd)) data = { 'Container': container, 'User': '', 'Privileged': privileged, 'Tty': tty, 'AttachStdin': False, 'AttachStdout': stdout, 'AttachStderr': stderr, 'Cmd': cmd } url = self._url('/containers/{0}/exec'.format(container)) res = self._post_json(url, data=data) return self._result(res, True) def exec_inspect(self, exec_id): if utils.compare_version('1.15', self._version) < 0: raise errors.InvalidVersion('Exec is not supported in API < 1.15') if isinstance(exec_id, dict): exec_id = exec_id.get('Id') res = self._get(self._url("/exec/{0}/json".format(exec_id))) return self._result(res, True) def exec_resize(self, exec_id, height=None, width=None): if utils.compare_version('1.15', self._version) < 0: raise errors.InvalidVersion('Exec is not supported in API < 1.15') if isinstance(exec_id, dict): exec_id = exec_id.get('Id') params = {'h': height, 'w': width} url = self._url("/exec/{0}/resize".format(exec_id)) res = self._post(url, params=params) self._raise_for_status(res) def exec_start(self, exec_id, detach=False, tty=False, stream=False): if utils.compare_version('1.15', self._version) < 0: raise errors.InvalidVersion('Exec is not supported in API < 1.15') if isinstance(exec_id, dict): exec_id = exec_id.get('Id') data = { 'Tty': tty, 'Detach': detach } res = self._post_json(self._url('/exec/{0}/start'.format(exec_id)), data=data, stream=stream) self._raise_for_status(res) if stream: return self._multiplexed_response_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)] ) @check_resource def export(self, container): if isinstance(container, dict): container = container.get('Id') res = self._get(self._url("/containers/{0}/export".format(container)), stream=True) self._raise_for_status(res) return res.raw @check_resource 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 @check_resource def history(self, image): res = self._get(self._url("/images/{0}/history".format(image))) return self._result(res, True) def images(self, name=None, quiet=False, all=False, viz=False, filters=None): if viz: if utils.compare_version('1.7', self._version) >= 0: raise Exception('Viz output is not supported in API >= 1.7!') return self._result(self._get(self._url("images/viz"))) params = { 'filter': name, 'only_ids': 1 if quiet else 0, 'all': 1 if all else 0, } if filters: params['filters'] = utils.convert_filters(filters) res = self._result(self._get(self._url("/images/json"), params=params), True) if quiet: return [x['Id'] for x in res] return res def import_image(self, src=None, repository=None, tag=None, image=None): if src: if isinstance(src, six.string_types): try: result = self.import_image_from_file( src, repository=repository, tag=tag) except IOError: result = self.import_image_from_url( src, repository=repository, tag=tag) else: result = self.import_image_from_data( src, repository=repository, tag=tag) elif image: result = self.import_image_from_image( image, repository=repository, tag=tag) else: raise Exception("Must specify a src or image") return result def import_image_from_data(self, data, repository=None, tag=None): u = self._url("/images/create") params = { 'fromSrc': '-', 'repo': repository, 'tag': tag } headers = { 'Content-Type': 'application/tar', } return self._result( self._post(u, data=data, params=params, headers=headers)) def import_image_from_file(self, filename, repository=None, tag=None): u = self._url("/images/create") params = { 'fromSrc': '-', 'repo': repository, 'tag': tag } headers = { 'Content-Type': 'application/tar', } with open(filename, 'rb') as f: return self._result( self._post(u, data=f, params=params, headers=headers, timeout=None)) def import_image_from_stream(self, stream, repository=None, tag=None): u = self._url("/images/create") params = { 'fromSrc': '-', 'repo': repository, 'tag': tag } headers = { 'Content-Type': 'application/tar', 'Transfer-Encoding': 'chunked', } return self._result( self._post(u, data=stream, params=params, headers=headers)) def import_image_from_url(self, url, repository=None, tag=None): u = self._url("/images/create") params = { 'fromSrc': url, 'repo': repository, 'tag': tag } return self._result( self._post(u, data=None, params=params)) def import_image_from_image(self, image, repository=None, tag=None): u = self._url("/images/create") params = { 'fromImage': image, 'repo': repository, 'tag': tag } return self._result( self._post(u, data=None, params=params)) def info(self): return self._result(self._get(self._url("/info")), True) @check_resource 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/{0}/insert".format(image)) params = { 'url': url, 'path': path } return self._result(self._post(api_url, params=params)) @check_resource 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) @check_resource def inspect_image(self, image): if isinstance(image, dict): image = image.get('Id') return self._result( self._get(self._url("/images/{0}/json".format(image))), True ) @check_resource def kill(self, container, signal=None): if isinstance(container, dict): container = container.get('Id') url = self._url("/containers/{0}/kill".format(container)) params = {} if signal is not None: params['signal'] = signal res = self._post(url, params=params) 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, insecure_registry=False, dockercfg_path=None): # If we don't have any auth data so far, try reloading the config file # one more time in case anything showed up in there. # If dockercfg_path is passed check to see if the config file exists, # if so load that config. if dockercfg_path and os.path.exists(dockercfg_path): self._auth_configs = auth.load_config(dockercfg_path) elif not self._auth_configs: self._auth_configs = auth.load_config() registry = registry or auth.INDEX_URL authcfg = auth.resolve_authconfig(self._auth_configs, registry) # If we found an existing auth config for this registry and username # combination, we can return it immediately unless reauth is requested. if authcfg and authcfg.get('username', None) == username \ and not reauth: return authcfg req_data = { 'username': username, 'password': password, 'email': email, 'serveraddress': registry, } response = self._post_json(self._url('/auth'), data=req_data) if response.status_code == 200: self._auth_configs[registry] = req_data return self._result(response, json=True) @check_resource def logs(self, container, stdout=True, stderr=True, stream=False, timestamps=False, tail='all'): 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, } if utils.compare_version('1.13', self._version) >= 0: if tail != 'all' and (not isinstance(tail, int) or tail <= 0): tail = 'all' params['tail'] = tail url = self._url("/containers/{0}/logs".format(container)) res = self._get(url, params=params, stream=stream) if stream: return self._multiplexed_response_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, stderr=stderr, stream=stream, logs=True ) @check_resource def pause(self, container): if isinstance(container, dict): container = container.get('Id') url = self._url('/containers/{0}/pause'.format(container)) res = self._post(url) self._raise_for_status(res) def ping(self): return self._result(self._get(self._url('/_ping'))) @check_resource def port(self, container, private_port): if isinstance(container, dict): container = container.get('Id') res = self._get(self._url("/containers/{0}/json".format(container))) self._raise_for_status(res) json_ = res.json() s_port = str(private_port) h_ports = None # Port settings is None when the container is running with # network_mode=host. port_settings = json_.get('NetworkSettings', {}).get('Ports') if port_settings is None: return None h_ports = port_settings.get(s_port + '/udp') if h_ports is None: h_ports = port_settings.get(s_port + '/tcp') return h_ports def pull(self, repository, tag=None, stream=False, insecure_registry=False, auth_config=None): if not tag: repository, tag = utils.parse_repository_tag(repository) registry, repo_name = auth.resolve_repository_name( repository, insecure=insecure_registry ) if repo_name.count(":") == 1: repository, tag = repository.rsplit(":", 1) params = { 'tag': tag, 'fromImage': repository } headers = {} if utils.compare_version('1.5', self._version) >= 0: # If we don't have any auth data so far, try reloading the config # file one more time in case anything showed up in there. if auth_config is None: if not self._auth_configs: self._auth_configs = auth.load_config() authcfg = auth.resolve_authconfig(self._auth_configs, registry) # Do not fail here if no authentication exists for this # specific registry as we can have a readonly pull. Just # put the header if we can. if authcfg: # auth_config needs to be a dict in the format used by # auth.py username , password, serveraddress, email headers['X-Registry-Auth'] = auth.encode_header( authcfg ) else: headers['X-Registry-Auth'] = auth.encode_header(auth_config) response = self._post( self._url('/images/create'), params=params, headers=headers, stream=stream, timeout=None ) self._raise_for_status(response) if stream: return self._stream_helper(response) return self._result(response) def push(self, repository, tag=None, stream=False, insecure_registry=False): if not tag: repository, tag = utils.parse_repository_tag(repository) registry, repo_name = auth.resolve_repository_name( repository, insecure=insecure_registry ) u = self._url("/images/{0}/push".format(repository)) params = { 'tag': tag } headers = {} if utils.compare_version('1.5', self._version) >= 0: # If we don't have any auth data so far, try reloading the config # file one more time in case anything showed up in there. if not self._auth_configs: self._auth_configs = auth.load_config() authcfg = auth.resolve_authconfig(self._auth_configs, registry) # Do not fail here if no authentication exists for this specific # registry as we can have a readonly pull. Just put the header if # we can. if authcfg: headers['X-Registry-Auth'] = auth.encode_header(authcfg) response = self._post_json( u, None, headers=headers, stream=stream, params=params ) self._raise_for_status(response) if stream: return self._stream_helper(response) return self._result(response) @check_resource def remove_container(self, container, v=False, link=False, force=False): if isinstance(container, dict): container = container.get('Id') params = {'v': v, 'link': link, 'force': force} res = self._delete(self._url("/containers/" + container), params=params) self._raise_for_status(res) @check_resource def remove_image(self, image, force=False, noprune=False): if isinstance(image, dict): image = image.get('Id') params = {'force': force, 'noprune': noprune} res = self._delete(self._url("/images/" + image), params=params) self._raise_for_status(res) @check_resource def rename(self, container, name): if utils.compare_version('1.17', self._version) < 0: raise errors.InvalidVersion( 'rename was only introduced in API version 1.17' ) if isinstance(container, dict): container = container.get('Id') url = self._url("/containers/{0}/rename".format(container)) params = {'name': name} res = self._post(url, params=params) self._raise_for_status(res) @check_resource 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) @check_resource def restart(self, container, timeout=10): if isinstance(container, dict): container = container.get('Id') params = {'t': timeout} url = self._url("/containers/{0}/restart".format(container)) res = self._post(url, params=params) self._raise_for_status(res) def search(self, term): return self._result(self._get(self._url("/images/search"), params={'term': term}), True) @check_resource 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, 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): if utils.compare_version('1.10', self._version) < 0: if dns is not None: raise errors.InvalidVersion( 'dns is only supported for API version >= 1.10' ) if volumes_from is not None: raise errors.InvalidVersion( 'volumes_from is only supported for API version >= 1.10' ) if utils.compare_version('1.15', self._version) < 0: if security_opt is not None: raise errors.InvalidVersion( 'security_opt is only supported for API version >= 1.15' ) if ipc_mode: raise errors.InvalidVersion( 'ipc_mode is only supported for API version >= 1.15' ) if utils.compare_version('1.17', self._version) < 0: if read_only is not None: raise errors.InvalidVersion( 'read_only is only supported for API version >= 1.17' ) if pid_mode is not None: raise errors.InvalidVersion( 'pid_mode is only supported for API version >= 1.17' ) if utils.compare_version('1.18', self._version) < 0: if ulimits is not None: raise errors.InvalidVersion( 'ulimits is only supported for API version >= 1.18' ) start_config = utils.create_host_config( binds=binds, port_bindings=port_bindings, lxc_conf=lxc_conf, publish_all_ports=publish_all_ports, links=links, dns=dns, privileged=privileged, dns_search=dns_search, cap_add=cap_add, cap_drop=cap_drop, volumes_from=volumes_from, devices=devices, network_mode=network_mode, restart_policy=restart_policy, extra_hosts=extra_hosts, read_only=read_only, pid_mode=pid_mode, ipc_mode=ipc_mode, security_opt=security_opt, ulimits=ulimits ) if isinstance(container, dict): container = container.get('Id') url = self._url("/containers/{0}/start".format(container)) if not start_config: start_config = None elif utils.compare_version('1.15', self._version) > 0: warnings.warn( 'Passing host config parameters in start() is deprecated. ' 'Please use host_config in create_container instead!', DeprecationWarning ) res = self._post_json(url, data=start_config) self._raise_for_status(res) @check_resource def stats(self, container, decode=None): if utils.compare_version('1.17', self._version) < 0: raise errors.InvalidVersion( 'Stats retrieval is not supported in API < 1.17!') if isinstance(container, dict): container = container.get('Id') url = self._url("/containers/{0}/stats".format(container)) return self._stream_helper(self._get(url, stream=True), decode=decode) @check_resource def stop(self, container, timeout=10): if isinstance(container, dict): container = container.get('Id') params = {'t': timeout} url = self._url("/containers/{0}/stop".format(container)) res = self._post(url, params=params, timeout=(timeout + self.timeout)) self._raise_for_status(res) @check_resource def tag(self, image, repository, tag=None, force=False): params = { 'tag': tag, 'repo': repository, 'force': 1 if force else 0 } url = self._url("/images/{0}/tag".format(image)) res = self._post(url, params=params) self._raise_for_status(res) return res.status_code == 201 @check_resource def top(self, container): if isinstance(container, dict): container = container.get('Id') u = self._url("/containers/{0}/top".format(container)) return self._result(self._get(u), True) def version(self, api_version=True): url = self._url("/version", versioned_api=api_version) return self._result(self._get(url), json=True) @check_resource def unpause(self, container): if isinstance(container, dict): container = container.get('Id') url = self._url('/containers/{0}/unpause'.format(container)) res = self._post(url) self._raise_for_status(res) @check_resource def wait(self, container, timeout=None): if isinstance(container, dict): container = container.get('Id') url = self._url("/containers/{0}/wait".format(container)) res = self._post(url, timeout=timeout) self._raise_for_status(res) json_ = res.json() if 'StatusCode' in json_: return json_['StatusCode'] return -1 class AutoVersionClient(Client): def __init__(self, *args, **kwargs): if 'version' in kwargs and kwargs['version']: raise errors.DockerException( 'Can not specify version for AutoVersionClient' ) kwargs['version'] = 'auto' super(AutoVersionClient, self).__init__(*args, **kwargs)