| # Licensed to the Apache Software Foundation (ASF) under one or more |
| # contributor license agreements. See the NOTICE file distributed with |
| # this work for additional information regarding copyright ownership. |
| # The ASF licenses this file to You 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 base64 |
| import datetime |
| import shlex |
| import re |
| |
| try: |
| import simplejson as json |
| except: |
| import json |
| |
| from libcloud.utils.py3 import httplib |
| from libcloud.utils.py3 import b |
| |
| from libcloud.common.base import JsonResponse, ConnectionUserAndKey |
| from libcloud.common.types import InvalidCredsError |
| |
| from libcloud.container.base import (Container, ContainerDriver, |
| ContainerImage) |
| |
| from libcloud.container.providers import Provider |
| from libcloud.container.types import ContainerState |
| |
| |
| VALID_RESPONSE_CODES = [httplib.OK, httplib.ACCEPTED, httplib.CREATED, |
| httplib.NO_CONTENT] |
| |
| |
| class DockerResponse(JsonResponse): |
| |
| valid_response_codes = [httplib.OK, httplib.ACCEPTED, httplib.CREATED, |
| httplib.NO_CONTENT] |
| |
| def parse_body(self): |
| if len(self.body) == 0 and not self.parse_zero_length_body: |
| return self.body |
| |
| try: |
| # error responses are tricky in Docker. Eg response could be |
| # an error, but response status could still be 200 |
| content_type = self.headers.get('content-type', 'application/json') |
| if content_type == 'application/json' or content_type == '': |
| body = json.loads(self.body) |
| else: |
| body = self.body |
| except ValueError: |
| m = re.search('Error: (.+?)"', self.body) |
| if m: |
| error_msg = m.group(1) |
| raise Exception(error_msg) |
| else: |
| raise Exception( |
| 'ConnectionError: Failed to parse JSON response') |
| return body |
| |
| def parse_error(self): |
| if self.status == 401: |
| raise InvalidCredsError('Invalid credentials') |
| return self.body |
| |
| def success(self): |
| return self.status in self.valid_response_codes |
| |
| |
| class DockerException(Exception): |
| |
| def __init__(self, code, message): |
| self.code = code |
| self.message = message |
| self.args = (code, message) |
| |
| def __str__(self): |
| return "%s %s" % (self.code, self.message) |
| |
| def __repr__(self): |
| return "DockerException %s %s" % (self.code, self.message) |
| |
| |
| class DockerConnection(ConnectionUserAndKey): |
| |
| responseCls = DockerResponse |
| timeout = 60 |
| |
| def add_default_headers(self, headers): |
| """ |
| Add parameters that are necessary for every request |
| If user and password are specified, include a base http auth |
| header |
| """ |
| headers['Content-Type'] = 'application/json' |
| if self.user_id and self.key: |
| user_b64 = base64.b64encode(b('%s:%s' % (self.user_id, self.key))) |
| headers['Authorization'] = 'Basic %s' % (user_b64.decode('utf-8')) |
| return headers |
| |
| |
| class DockerContainerDriver(ContainerDriver): |
| """ |
| Docker container driver class. |
| |
| >>> from libcloud.container.providers import get_driver |
| >>> driver = get_driver('docker') |
| >>> conn = driver(host='198.61.239.128', port=4243) |
| >>> conn.list_containers() |
| or connecting to http basic auth protected https host: |
| >>> conn = driver('user', 'pass', host='https://198.61.239.128', port=443) |
| |
| connect with tls authentication, by providing a hostname, port, a private |
| key file (.pem) and certificate (.pem) file |
| >>> conn = driver(host='https://198.61.239.128', |
| >>> port=4243, key_file='key.pem', cert_file='cert.pem') |
| """ |
| |
| type = Provider.DOCKER |
| name = 'Docker' |
| website = 'http://docker.io' |
| connectionCls = DockerConnection |
| supports_clusters = False |
| version = '1.24' |
| |
| def __init__(self, key=None, secret=None, secure=False, host='localhost', |
| port=4243, key_file=None, cert_file=None): |
| """ |
| :param key: API key or username to used (required) |
| :type key: ``str`` |
| |
| :param secret: Secret password to be used (required) |
| :type secret: ``str`` |
| |
| :param secure: Whether to use HTTPS or HTTP. Note: Some providers |
| only support HTTPS, and it is on by default. |
| :type secure: ``bool`` |
| |
| :param host: Override hostname used for connections. |
| :type host: ``str`` |
| |
| :param port: Override port used for connections. |
| :type port: ``int`` |
| |
| :param key_file: Path to private key for TLS connection (optional) |
| :type key_file: ``str`` |
| |
| :param cert_file: Path to public key for TLS connection (optional) |
| :type cert_file: ``str`` |
| |
| :return: ``None`` |
| """ |
| super(DockerContainerDriver, self).__init__(key=key, secret=secret, |
| secure=secure, host=host, |
| port=port, |
| key_file=key_file, |
| cert_file=cert_file) |
| if host.startswith('https://'): |
| secure = True |
| |
| # strip the prefix |
| prefixes = ['http://', 'https://'] |
| for prefix in prefixes: |
| if host.startswith(prefix): |
| host = host.strip(prefix) |
| |
| if key_file or cert_file: |
| # docker tls authentication- |
| # https://docs.docker.com/articles/https/ |
| # We pass two files, a key_file with the |
| # private key and cert_file with the certificate |
| # libcloud will handle them through LibcloudHTTPSConnection |
| if not (key_file and cert_file): |
| raise Exception( |
| 'Needs both private key file and ' |
| 'certificate file for tls authentication') |
| self.connection.key_file = key_file |
| self.connection.cert_file = cert_file |
| self.connection.secure = True |
| else: |
| self.connection.secure = secure |
| |
| self.connection.host = host |
| self.connection.port = port |
| |
| def install_image(self, path): |
| """ |
| Install a container image from a remote path. |
| |
| :param path: Path to the container image |
| :type path: ``str`` |
| |
| :rtype: :class:`libcloud.container.base.ContainerImage` |
| """ |
| payload = { |
| } |
| data = json.dumps(payload) |
| |
| result = self.connection.request('v%s/images/create?fromImage=%s' % |
| (self.version, path), data=data, |
| method='POST') |
| if "errorDetail" in result.body: |
| raise DockerException(None, result.body) |
| try: |
| # get image id |
| image_id = re.findall( |
| r'{"status":"Download complete"' |
| r',"progressDetail":{},"id":"\w+"}', |
| result.body)[-1] |
| image_id = json.loads(image_id).get('id') |
| except: |
| raise DockerException(None, 'failed to install image') |
| |
| image = ContainerImage( |
| id=image_id, |
| name=path, |
| path=path, |
| version=None, |
| driver=self.connection.driver, |
| extra={}) |
| return image |
| |
| def list_images(self): |
| """ |
| List the installed container images |
| |
| :rtype: ``list`` of :class:`libcloud.container.base.ContainerImage` |
| """ |
| result = self.connection.request('v%s/images/json' % |
| (self.version)).object |
| images = [] |
| for image in result: |
| try: |
| name = image.get('RepoTags')[0] |
| except: |
| name = image.get('Id') |
| images.append(ContainerImage( |
| id=image.get('Id'), |
| name=name, |
| path=name, |
| version=None, |
| driver=self.connection.driver, |
| extra={ |
| "created": image.get('Created'), |
| "size": image.get('Size'), |
| "virtual_size": image.get('VirtualSize'), |
| }, |
| )) |
| |
| return images |
| |
| def list_containers(self, image=None, all=True): |
| """ |
| List the deployed container images |
| |
| :param image: Filter to containers with a certain image |
| :type image: :class:`libcloud.container.base.ContainerImage` |
| |
| :param all: Show all container (including stopped ones) |
| :type all: ``bool`` |
| |
| :rtype: ``list`` of :class:`libcloud.container.base.Container` |
| """ |
| if all: |
| ex = '?all=1' |
| else: |
| ex = '' |
| try: |
| result = self.connection.request( |
| "v%s/containers/json%s" % (self.version, ex)).object |
| except Exception as exc: |
| errno = getattr(exc, 'errno', None) |
| if errno == 111: |
| raise DockerException( |
| errno, |
| 'Make sure docker host is accessible' |
| 'and the API port is correct') |
| raise |
| |
| containers = [self._to_container(value) for value in result] |
| return containers |
| |
| def deploy_container(self, name, image, parameters=None, start=True, |
| command=None, hostname=None, user='', |
| stdin_open=True, tty=True, |
| mem_limit=0, ports=None, environment=None, dns=None, |
| volumes=None, volumes_from=None, |
| network_disabled=False, entrypoint=None, |
| cpu_shares=None, working_dir='', domainname=None, |
| memswap_limit=0, port_bindings=None, |
| network_mode='bridge', labels=None): |
| """ |
| Deploy an installed container image |
| |
| For details on the additional parameters see : http://bit.ly/1PjMVKV |
| |
| :param name: The name of the new container |
| :type name: ``str`` |
| |
| :param image: The container image to deploy |
| :type image: :class:`libcloud.container.base.ContainerImage` |
| |
| :param parameters: Container Image parameters |
| :type parameters: ``str`` |
| |
| :param start: Start the container on deployment |
| :type start: ``bool`` |
| |
| :rtype: :class:`Container` |
| """ |
| command = shlex.split(str(command)) |
| if port_bindings is None: |
| port_bindings = {} |
| params = { |
| 'name': name |
| } |
| |
| payload = { |
| 'Hostname': hostname, |
| 'Domainname': domainname, |
| 'ExposedPorts': ports, |
| 'User': user, |
| 'Tty': tty, |
| 'OpenStdin': stdin_open, |
| 'StdinOnce': False, |
| 'Memory': mem_limit, |
| 'AttachStdin': True, |
| 'AttachStdout': True, |
| 'AttachStderr': True, |
| 'Env': environment, |
| 'Cmd': command, |
| 'Dns': dns, |
| 'Image': image.name, |
| 'Volumes': volumes, |
| 'VolumesFrom': volumes_from, |
| 'NetworkDisabled': network_disabled, |
| 'Entrypoint': entrypoint, |
| 'CpuShares': cpu_shares, |
| 'WorkingDir': working_dir, |
| 'MemorySwap': memswap_limit, |
| 'PublishAllPorts': True, |
| 'PortBindings': port_bindings, |
| 'NetworkMode': network_mode, |
| 'Labels': labels, |
| } |
| |
| data = json.dumps(payload) |
| try: |
| result = self.connection.request('v%s/containers/create' |
| % (self.version), |
| data=data, |
| params=params, method='POST') |
| except Exception as e: |
| message = e.message or str(e) |
| if message.startswith('No such image:'): |
| raise DockerException(None, 'No such image: %s' % image.name) |
| else: |
| raise DockerException(None, e) |
| |
| id_ = result.object['Id'] |
| |
| payload = { |
| 'Binds': [], |
| 'PublishAllPorts': True, |
| 'PortBindings': port_bindings, |
| } |
| |
| data = json.dumps(payload) |
| if start: |
| result = self.connection.request( |
| 'v%s/containers/%s/start' % |
| (self.version, id_), data=data, |
| method='POST') |
| |
| return self.get_container(id_) |
| |
| def get_container(self, id): |
| """ |
| Get a container by ID |
| |
| :param id: The ID of the container to get |
| :type id: ``str`` |
| |
| :rtype: :class:`libcloud.container.base.Container` |
| """ |
| result = self.connection.request("v%s/containers/%s/json" % |
| (self.version, id)).object |
| |
| return self._to_container(result) |
| |
| def start_container(self, container): |
| """ |
| Start a container |
| |
| :param container: The container to be started |
| :type container: :class:`libcloud.container.base.Container` |
| |
| :return: The container refreshed with current data |
| :rtype: :class:`libcloud.container.base.Container` |
| """ |
| payload = { |
| 'Binds': [], |
| 'PublishAllPorts': True, |
| } |
| data = json.dumps(payload) |
| result = self.connection.request( |
| 'v%s/containers/%s/start' % |
| (self.version, container.id), |
| method='POST', data=data) |
| if result.status in VALID_RESPONSE_CODES: |
| return self.get_container(container.id) |
| else: |
| raise DockerException(result.status, |
| 'failed to start container') |
| |
| def stop_container(self, container): |
| """ |
| Stop a container |
| |
| :param container: The container to be stopped |
| :type container: :class:`libcloud.container.base.Container` |
| |
| :return: The container refreshed with current data |
| :rtype: :class:`libcloud.container.base.Container` |
| """ |
| result = self.connection.request('v%s/containers/%s/stop' % |
| (self.version, container.id), |
| method='POST') |
| if result.status in VALID_RESPONSE_CODES: |
| return self.get_container(container.id) |
| else: |
| raise DockerException(result.status, |
| 'failed to stop container') |
| |
| def restart_container(self, container): |
| """ |
| Restart a container |
| |
| :param container: The container to be stopped |
| :type container: :class:`libcloud.container.base.Container` |
| |
| :return: The container refreshed with current data |
| :rtype: :class:`libcloud.container.base.Container` |
| """ |
| data = json.dumps({'t': 10}) |
| # number of seconds to wait before killing the container |
| result = self.connection.request('v%s/containers/%s/restart' % |
| (self.version, container.id), |
| data=data, method='POST') |
| if result.status in VALID_RESPONSE_CODES: |
| return self.get_container(container.id) |
| else: |
| raise DockerException(result.status, |
| 'failed to restart container') |
| |
| def destroy_container(self, container): |
| """ |
| Remove a container |
| |
| :param container: The container to be destroyed |
| :type container: :class:`libcloud.container.base.Container` |
| |
| :return: True if the destroy was successful, False otherwise. |
| :rtype: ``bool`` |
| """ |
| result = self.connection.request('v%s/containers/%s' % (self.version, |
| container.id), |
| method='DELETE') |
| return result.status in VALID_RESPONSE_CODES |
| |
| def ex_list_processes(self, container): |
| """ |
| List processes running inside a container |
| |
| :param container: The container to list processes for. |
| :type container: :class:`libcloud.container.base.Container` |
| |
| :rtype: ``str`` |
| """ |
| result = self.connection.request("v%s/containers/%s/top" % |
| (self.version, container.id)).object |
| |
| return result |
| |
| def ex_rename_container(self, container, name): |
| """ |
| Rename a container |
| |
| :param container: The container to be renamed |
| :type container: :class:`libcloud.container.base.Container` |
| |
| :param name: The new name |
| :type name: ``str`` |
| |
| :rtype: :class:`libcloud.container.base.Container` |
| """ |
| result = self.connection.request('v%s/containers/%s/rename?name=%s' |
| % (self.version, container.id, name), |
| method='POST') |
| if result.status in VALID_RESPONSE_CODES: |
| return self.get_container(container.id) |
| |
| def ex_get_logs(self, container, stream=False): |
| """ |
| Get container logs |
| |
| If stream == True, logs will be yielded as a stream |
| From Api Version 1.11 and above we need a GET request to get the logs |
| Logs are in different format of those of Version 1.10 and below |
| |
| :param container: The container to list logs for |
| :type container: :class:`libcloud.container.base.Container` |
| |
| :param stream: Stream the output |
| :type stream: ``bool`` |
| |
| :rtype: ``bool`` |
| """ |
| payload = {} |
| data = json.dumps(payload) |
| |
| if float(self._get_api_version()) > 1.10: |
| result = self.connection.request( |
| "v%s/containers/%s/logs?follow=%s&stdout=1&stderr=1" % |
| (self.version, container.id, str(stream))).object |
| logs = result |
| else: |
| result = self.connection.request( |
| "v%s/containers/%s/attach?logs=1&stream=%s&stdout=1&stderr=1" % |
| (self.version, container.id, str(stream)), |
| method='POST', |
| data=data) |
| logs = result.body |
| |
| return logs |
| |
| def ex_search_images(self, term): |
| """Search for an image on Docker.io. |
| Returns a list of ContainerImage objects |
| |
| >>> images = conn.ex_search_images(term='mistio') |
| >>> images |
| [<ContainerImage: id=rolikeusch/docker-mistio...>, |
| <ContainerImage: id=mist/mistio, name=mist/mistio, |
| driver=Docker ...>] |
| |
| :param term: The search term |
| :type term: ``str`` |
| |
| :rtype: ``list`` of :class:`libcloud.container.base.ContainerImage` |
| """ |
| |
| term = term.replace(' ', '+') |
| result = self.connection.request('v%s/images/search?term=%s' % |
| (self.version, term)).object |
| images = [] |
| for image in result: |
| name = image.get('name') |
| images.append( |
| ContainerImage( |
| id=name, |
| path=name, |
| version=None, |
| name=name, |
| driver=self.connection.driver, |
| extra={ |
| "description": image.get('description'), |
| "is_official": image.get('is_official'), |
| "is_trusted": image.get('is_trusted'), |
| "star_count": image.get('star_count'), |
| }, |
| )) |
| |
| return images |
| |
| def ex_delete_image(self, image): |
| """ |
| Remove image from the filesystem |
| |
| :param image: The image to remove |
| :type image: :class:`libcloud.container.base.ContainerImage` |
| |
| :rtype: ``bool`` |
| """ |
| result = self.connection.request('v%s/images/%s' % (self.version, |
| image.name), |
| method='DELETE') |
| return result.status in VALID_RESPONSE_CODES |
| |
| def _to_container(self, data): |
| """ |
| Convert container in Container instances |
| """ |
| try: |
| name = data.get('Name').strip('/') |
| except: |
| try: |
| name = data.get('Names')[0].strip('/') |
| except: |
| name = data.get('Id') |
| state = data.get('State') |
| if isinstance(state, dict): |
| status = data.get( |
| 'Status', |
| state.get('Status') |
| if state is not None else None) |
| else: |
| status = data.get('Status') |
| if 'Exited' in status: |
| state = ContainerState.STOPPED |
| elif status.startswith('Up '): |
| state = ContainerState.RUNNING |
| else: |
| state = ContainerState.STOPPED |
| image = data.get('Image') |
| ports = data.get('Ports', []) |
| created = data.get('Created') |
| if isinstance(created, float): |
| created = ts_to_str(created) |
| extra = { |
| 'id': data.get('Id'), |
| 'status': data.get('Status'), |
| 'created': created, |
| 'image': image, |
| 'ports': ports, |
| 'command': data.get('Command'), |
| 'sizerw': data.get('SizeRw'), |
| 'sizerootfs': data.get('SizeRootFs'), |
| } |
| ips = [] |
| if ports is not None: |
| for port in ports: |
| if port.get('IP') is not None: |
| ips.append(port.get('IP')) |
| return Container( |
| id=data['Id'], |
| name=name, |
| image=ContainerImage( |
| id=data.get('ImageID', None), |
| path=image, |
| name=image, |
| version=None, |
| driver=self.connection.driver |
| ), |
| ip_addresses=ips, |
| state=state, |
| driver=self.connection.driver, |
| extra=extra) |
| |
| def _get_api_version(self): |
| """ |
| Get the docker API version information |
| """ |
| result = self.connection.request('/version').object |
| result = result or {} |
| api_version = result.get('ApiVersion') |
| |
| return api_version |
| |
| |
| def ts_to_str(timestamp): |
| """ |
| Return a timestamp as a nicely formated datetime string. |
| """ |
| date = datetime.datetime.fromtimestamp(timestamp) |
| date_string = date.strftime("%d/%m/%Y %H:%M %Z") |
| return date_string |