summaryrefslogtreecommitdiffstats
path: root/python-dockering
diff options
context:
space:
mode:
authorTommy Carpenter <tommy@research.att.com>2017-08-23 11:21:44 -0400
committerTommy Carpenter <tommy@research.att.com>2017-08-23 12:32:45 -0400
commit81b9ed7a3af1032caa86f316a21e9b2912bb1271 (patch)
treefe3bb349f501619d3b4df4d4dbb07b9d72adc824 /python-dockering
parent280af47e5342de6bb4657ad7b85adcc9395ace20 (diff)
Intiial utils push to ONAP
Issue-Id: DCAEGEN2-80 Change-Id: I1dc8f2d384b0af346fccb86e6767b7e3ca484078 Signed-off-by: Tommy Carpenter <tommy@research.att.com>
Diffstat (limited to 'python-dockering')
-rw-r--r--python-dockering/.gitignore67
-rw-r--r--python-dockering/ChangeLog.md11
-rw-r--r--python-dockering/LICENSE.txt32
-rw-r--r--python-dockering/README.md3
-rw-r--r--python-dockering/dockering/__init__.py21
-rw-r--r--python-dockering/dockering/config_building.py269
-rw-r--r--python-dockering/dockering/core.py136
-rw-r--r--python-dockering/dockering/exceptions.py34
-rw-r--r--python-dockering/dockering/utils.py31
-rw-r--r--python-dockering/setup.py34
-rw-r--r--python-dockering/tests/test_config_building.py150
-rw-r--r--python-dockering/tests/test_core.py71
12 files changed, 859 insertions, 0 deletions
diff --git a/python-dockering/.gitignore b/python-dockering/.gitignore
new file mode 100644
index 0000000..d11997c
--- /dev/null
+++ b/python-dockering/.gitignore
@@ -0,0 +1,67 @@
+.cloudify
+*.swp
+*.swn
+*.swo
+.DS_Store
+.project
+.pydevproject
+venv
+
+
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+env/
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+*.egg-info/
+.installed.cfg
+*.egg
+
+# PyInstaller
+# Usually these files are written by a python script from a template
+# before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*,cover
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+target/
diff --git a/python-dockering/ChangeLog.md b/python-dockering/ChangeLog.md
new file mode 100644
index 0000000..67ae4f8
--- /dev/null
+++ b/python-dockering/ChangeLog.md
@@ -0,0 +1,11 @@
+# Change Log
+
+All notable changes to this project will be documented in this file.
+
+The format is based on [Keep a Changelog](http://keepachangelog.com/)
+and this project adheres to [Semantic Versioning](http://semver.org/).
+
+## [1.2.0]
+
+* Add the ability to force reauthentication for Docker login
+* Handle connection errors for Docker login
diff --git a/python-dockering/LICENSE.txt b/python-dockering/LICENSE.txt
new file mode 100644
index 0000000..cb8008a
--- /dev/null
+++ b/python-dockering/LICENSE.txt
@@ -0,0 +1,32 @@
+============LICENSE_START=======================================================
+org.onap.dcae
+================================================================================
+Copyright (c) 2017 AT&T Intellectual Property. All rights reserved.
+================================================================================
+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.
+============LICENSE_END=========================================================
+
+ECOMP is a trademark and service mark of AT&T Intellectual Property.
+
+
+Copyright (c) 2017 AT&T Intellectual Property. All rights reserved.
+===================================================================
+Licensed under the Creative Commons License, Attribution 4.0 Intl. (the "License");
+you may not use this documentation except in compliance with the License.
+You may obtain a copy of the License at
+ https://creativecommons.org/licenses/by/4.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.
diff --git a/python-dockering/README.md b/python-dockering/README.md
new file mode 100644
index 0000000..fd8d436
--- /dev/null
+++ b/python-dockering/README.md
@@ -0,0 +1,3 @@
+# python-dockering
+
+Library used to manage Docker containers in DCAE.
diff --git a/python-dockering/dockering/__init__.py b/python-dockering/dockering/__init__.py
new file mode 100644
index 0000000..7c248d8
--- /dev/null
+++ b/python-dockering/dockering/__init__.py
@@ -0,0 +1,21 @@
+# org.onap.dcae
+# ================================================================================
+# Copyright (c) 2017 AT&T Intellectual Property. All rights reserved.
+# ================================================================================
+# 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.
+# ============LICENSE_END=========================================================
+#
+# ECOMP is a trademark and service mark of AT&T Intellectual Property.
+
+from dockering.core import *
+from dockering.config_building import *
diff --git a/python-dockering/dockering/config_building.py b/python-dockering/dockering/config_building.py
new file mode 100644
index 0000000..d8e3c84
--- /dev/null
+++ b/python-dockering/dockering/config_building.py
@@ -0,0 +1,269 @@
+# org.onap.dcae
+# ================================================================================
+# Copyright (c) 2017 AT&T Intellectual Property. All rights reserved.
+# ================================================================================
+# 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.
+# ============LICENSE_END=========================================================
+#
+# ECOMP is a trademark and service mark of AT&T Intellectual Property.
+
+"""
+Abstraction in Docker container configuration
+"""
+from dockering import utils
+from dockering.exceptions import DockerConstructionError
+
+
+#
+# Methods to build container envs
+#
+
+def create_envs_healthcheck(docker_config, default_interval="15s",
+ default_timeout="1s"):
+ """Extract health check environment variables for Docker containers
+
+ Parameters
+ ----------
+ docker_config: dict where there's an entry called "healthcheck"
+
+ Returns
+ -------
+ dict of Docker envs for healthcheck
+ """
+ # TODO: This has been shamefully lifted from the dcae-cli and should probably
+ # shared as a library. The unit tests are there. The difference is that
+ # there are defaults that are passed in here but the defaults should really
+ # come from the component spec definition. The issue is that nothing forwards
+ # those defaults.
+
+ envs = dict()
+ hc = docker_config["healthcheck"]
+
+ # NOTE: For the multiple port, schema scenario, you can explicitly set port
+ # to schema. For example if image EXPOSE 8080, SERVICE_8080_CHECK_HTTP works.
+ # https://github.com/gliderlabs/registrator/issues/311
+
+ if hc["type"] == "http":
+ envs["SERVICE_CHECK_HTTP"] = hc["endpoint"]
+ elif hc["type"] == "https":
+ # WATCH: HTTPS health checks don't work. Seems like Registrator bug.
+ # Submitted issue https://github.com/gliderlabs/registrator/issues/516
+ envs["SERVICE_CHECK_HTTPS"] = hc["endpoint"]
+ utils.logger.warn("Https-based health checks may not work because Registrator issue #516")
+ elif hc["type"] == "script":
+ envs["SERVICE_CHECK_SCRIPT"] = hc["script"]
+ elif hc["type"] == "docker":
+ # Note this is only supported in the AT&T open source version of registrator
+ envs["SERVICE_CHECK_DOCKER_SCRIPT"] = hc["script"]
+ else:
+ # You should never get here but not having an else block feels weird
+ raise DockerConstructionError("Unexpected health check type: {0}".format(hc["type"]))
+
+ envs["SERVICE_CHECK_INTERVAL"] = hc.get("interval", default_interval)
+ envs["SERVICE_CHECK_TIMEOUT"] = hc.get("timeout", default_timeout)
+
+ return envs
+
+
+def create_envs(service_component_name, *envs):
+ """Merge all environment variables maps
+
+ Creates a complete environment variables map that is to be used for creating
+ the container.
+
+ Args:
+ -----
+ envs: Arbitrary list of dicts where each dict is of the structure:
+
+ {
+ <environment variable name>: <environment variable value>,
+ <environment variable name>: <environment variable value>,
+ ...
+ }
+
+ Returns:
+ --------
+ Dict of all environment variable name to environment variable value
+ """
+ master_envs = { "HOSTNAME": service_component_name,
+ # For Registrator to register with generated name and not the
+ # image name
+ "SERVICE_NAME": service_component_name }
+ for envs_map in envs:
+ master_envs.update(envs_map)
+ return master_envs
+
+
+#
+# Methods for volume bindings
+#
+
+def _parse_volumes_param(volumes):
+ """Parse volumes details for Docker containers from blueprint
+
+ Takes in a list of dicts that contains Docker volume info and
+ transforms them into docker-py compliant (unflattened) data structures.
+ Look for the `volumes` parameters under the `run` method on
+ [this page](https://docker-py.readthedocs.io/en/stable/containers.html)
+
+ Args:
+ volumes (list): List of
+
+ {
+ "host": {
+ "path": <target path on host>
+ },
+ "container": {
+ "bind": <target path in container>,
+ "mode": <read/write>
+ }
+ }
+
+ Returns:
+ dict of the form
+
+ {
+ <target path on host>: {
+ "bind": <target path in container>,
+ "mode": <read/write>
+ }
+ }
+
+ if volumes is None then returns None
+ """
+ if volumes:
+ return dict([ (vol["host"]["path"], vol["container"]) for vol in volumes ])
+ else:
+ return None
+
+
+#
+# Utility methods used to help build the inputs to create the host_config
+#
+
+def add_host_config_params_volumes(volumes=None, host_config_params=None):
+ """Add volumes input params
+
+ Args:
+ -----
+ volumes (list): List of
+
+ {
+ "host": {
+ "path": <target path on host>
+ },
+ "container": {
+ "bind": <target path in container>,
+ "mode": <read/write>
+ }
+ }
+
+ host_config_params (dict): Target dict to accumulate host config inputs
+
+ Returns:
+ --------
+ Updated host_config_params
+ """
+# TODO: USE parse_volumes_param here!
+ if host_config_params == None:
+ host_config_params = {}
+
+ host_config_params["binds"] = _parse_volumes_param(volumes)
+ return host_config_params
+
+def add_host_config_params_ports(ports=None, host_config_params=None):
+ """Add ports input params
+
+ Args:
+ -----
+ ports (list): Each port mapping entry is of the form
+
+ "<container ports>:<host port>"
+
+ host_config_params (dict): Target dict to accumulate host config inputs
+
+ Returns:
+ --------
+ Updated host_config_params
+ """
+ if host_config_params == None:
+ host_config_params = {}
+
+ if ports:
+ ports = [ port.split(":") for port in ports ]
+ port_bindings = { port[0]: { "HostPort": port[1] } for port in ports }
+ host_config_params["port_bindings"] = port_bindings
+ host_config_params["publish_all_ports"] = False
+ else:
+ host_config_params["publish_all_ports"] = True
+
+ return host_config_params
+
+def add_host_config_params_dns(docker_host, host_config_params=None):
+ """Add dns input params
+
+ This is not a generic implementation. This method will setup dns with the
+ expectation that a local consul agent is running on the docker host and will
+ service the dns requests.
+
+ Args:
+ -----
+ docker_host (string): Docker host ip address which will be used as the dns server
+ host_config_params (dict): Target dict to accumulate host config inputs
+
+ Returns:
+ --------
+ Updated host_config_params
+ """
+ if host_config_params == None:
+ host_config_params = {}
+
+ host_config_params["dns"] = [docker_host]
+ host_config_params["dns_search"] = ["service.consul"]
+ host_config_params["extra_hosts"] = { "consul": docker_host }
+ return host_config_params
+
+
+def create_container_config(client, image, envs, host_config_params, tty=False):
+ """Create docker container config
+
+ Args:
+ -----
+ envs (dict): dict of environment variables to pass into the docker containers.
+ Gets passed into docker-py.create_container call
+ host_config_params (dict): Dict of input parameters to the docker-py
+ "create_host_config" method call
+ """
+ # This is the 1.10.6 approach to binding volumes
+ # http://docker-py.readthedocs.io/en/1.10.6/volumes.html
+ volumes = host_config_params.get("bind", None)
+ target_volumes = [ target["bind"] for target in volumes.values() ] \
+ if volumes else None
+
+ host_config = client.create_host_config(**host_config_params)
+
+ if "port_bindings" in host_config_params:
+ # TODO: Use six for creating the list of ports - six.iterkeys
+ ports = host_config_params["port_bindings"].keys()
+ else:
+ ports = None
+
+ command = "" # This is required...
+ config = client.create_container_config(image, command, detach=True, tty=tty,
+ host_config=host_config, ports=ports,
+ environment=envs, volumes=target_volumes)
+
+ utils.logger.info("Docker container config: {0}".format(config))
+
+ return config
+
diff --git a/python-dockering/dockering/core.py b/python-dockering/dockering/core.py
new file mode 100644
index 0000000..dcd5908
--- /dev/null
+++ b/python-dockering/dockering/core.py
@@ -0,0 +1,136 @@
+# org.onap.dcae
+# ================================================================================
+# Copyright (c) 2017 AT&T Intellectual Property. All rights reserved.
+# ================================================================================
+# 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.
+# ============LICENSE_END=========================================================
+#
+# ECOMP is a trademark and service mark of AT&T Intellectual Property.
+
+import json
+import docker
+import requests
+from dockering.exceptions import DockerError, DockerConnectionError
+from dockering import config_building as cb
+from dockering import utils
+
+
+# TODO: Replace default for logins to source it from Consul..perhaps
+
+def create_client(hostname, port, reauth=False, logins=[]):
+ """Create Docker client
+
+ Args:
+ -----
+ reauth: (boolean) Forces reauthentication e.g. Docker login
+ """
+ base_url = "tcp://{0}:{1}".format(hostname, port)
+ try:
+ client = docker.Client(base_url=base_url)
+
+ for dcl in logins:
+ dcl["reauth"] = reauth
+ client.login(**dcl)
+
+ return client
+ except requests.exceptions.ConnectionError as e:
+ raise DockerConnectionError(str(e))
+
+
+def create_container_using_config(client, service_component_name, container_config):
+ try:
+ image_name = container_config["Image"]
+
+ if not client.images(image_name):
+ def parse_pull_response(response):
+ """Pull response is a giant string of JSON messages concatentated
+ by `\r\n`. This method returns back those messages in the form of
+ list of dicts."""
+ # NOTE: There's a trailing `\r\n` so the last element is empty
+ # string. Remove that.
+ return list(map(json.loads, response.split("\r\n")[:-1]))
+
+ def get_error_message(response):
+ """Attempts to pull out and return an error message from parsed
+ response if it exists else return None"""
+ return response[-1].get("error", None)
+
+ # TODO: Implement this as verbose?
+ # for resp in client.pull(image, stream=True, decode=True):
+ response = parse_pull_response(client.pull(image_name))
+ error_message = get_error_message(response)
+
+ if error_message:
+ raise DockerError("Error pulling Docker image: {0}".format(error_message))
+ else:
+ utils.logger.info("Pulled Docker image: {0}".format(image_name))
+
+ return client.create_container_from_config(container_config,
+ service_component_name)
+ except requests.exceptions.ConnectionError as e:
+ # This separates connection failures so that caller can decide what to do.
+ # Underlying errors this inspired were socket.errors that are sourced
+ # from http://www.virtsync.com/c-error-codes-include-errno
+ raise DockerConnectionError(str(e))
+ except Exception as e:
+ raise DockerError(str(e))
+
+
+def create_container(client, image_name, service_component_name, envs,
+ host_config_params):
+ """Creates Docker container
+
+ Args:
+ -----
+ envs (dict): dict of environment variables to pass into the docker containers.
+ Gets passed into docker-py.create_container call
+ host_config_params (dict): Dict of input parameters to the docker-py
+ "create_host_config" method call
+ """
+ config = cb.create_container_config(client, image_name, envs, host_config_params)
+ return create_container_using_config(client, service_component_name, config)
+
+
+def start_container(client, container):
+ try:
+ # TODO: Have logic to inspect response and through NonRecoverableError
+ # when start fails. Docker-py docs don't quickly tell me what the
+ # response looks like.
+ response = client.start(container=container["Id"])
+ utils.logger.info("Container started: {0}".format(container["Id"]))
+
+ # TODO: Maybe check stats?
+ return container["Id"]
+ except Exception as e:
+ raise DockerError(str(e))
+
+
+def stop_then_remove_container(client, service_component_name):
+ try:
+ client.stop(service_component_name)
+ client.remove_container(service_component_name)
+ except docker.errors.NotFound as e:
+ raise DockerError("Container not found: {0}".format(service_component_name))
+ except Exception as e:
+ raise DockerError(str(e))
+
+
+def remove_image(client, image_name):
+ """Remove the Docker image"""
+ try:
+ client.remove_image(image_name)
+ return True
+ except:
+ # Failure to remove image is not classified as terrible..for now
+ return False
+
diff --git a/python-dockering/dockering/exceptions.py b/python-dockering/dockering/exceptions.py
new file mode 100644
index 0000000..62ea145
--- /dev/null
+++ b/python-dockering/dockering/exceptions.py
@@ -0,0 +1,34 @@
+# org.onap.dcae
+# ================================================================================
+# Copyright (c) 2017 AT&T Intellectual Property. All rights reserved.
+# ================================================================================
+# 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.
+# ============LICENSE_END=========================================================
+#
+# ECOMP is a trademark and service mark of AT&T Intellectual Property.
+
+"""
+Library's exceptions
+"""
+
+class DockerError(RuntimeError):
+ """General error"""
+ pass
+
+class DockerConnectionError(DockerError):
+ """Errors connecting to the Docker engine"""
+ pass
+
+class DockerConstructionError(DockerError):
+ """This class of error captures failures in trying to setup the container"""
+ pass
diff --git a/python-dockering/dockering/utils.py b/python-dockering/dockering/utils.py
new file mode 100644
index 0000000..e0f651e
--- /dev/null
+++ b/python-dockering/dockering/utils.py
@@ -0,0 +1,31 @@
+# org.onap.dcae
+# ================================================================================
+# Copyright (c) 2017 AT&T Intellectual Property. All rights reserved.
+# ================================================================================
+# 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.
+# ============LICENSE_END=========================================================
+#
+# ECOMP is a trademark and service mark of AT&T Intellectual Property.
+
+"""
+Utility module
+"""
+import logging
+
+
+# Unified all logging through this single logger in order to easily monkeypatch
+# this guy in the Cloudify docker plugin. I also tried monkeypatching a getter
+# function that returns a logger but that didn't work.
+# WATCH! The monkeypatching in the Cloudify plugin will not work if you import
+# this logger with the following syntax: from dockering.utils import logger.
+logger = logging.getLogger("dockering")
diff --git a/python-dockering/setup.py b/python-dockering/setup.py
new file mode 100644
index 0000000..1c51ab9
--- /dev/null
+++ b/python-dockering/setup.py
@@ -0,0 +1,34 @@
+# org.onap.dcae
+# ================================================================================
+# Copyright (c) 2017 AT&T Intellectual Property. All rights reserved.
+# ================================================================================
+# 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.
+# ============LICENSE_END=========================================================
+#
+# ECOMP is a trademark and service mark of AT&T Intellectual Property.
+
+import os
+from setuptools import setup
+
+setup(
+ name='python-dockering',
+ description='Library used to manage Docker containers in DCAE',
+ version="1.2.0",
+ author="Michael Hwang",
+ email="dcae@lists.openecomp.org",
+ packages=['dockering'],
+ zip_safe=False,
+ install_requires=[
+ "docker-py>=1.0.0,<2.0.0"
+ ]
+)
diff --git a/python-dockering/tests/test_config_building.py b/python-dockering/tests/test_config_building.py
new file mode 100644
index 0000000..c9251e2
--- /dev/null
+++ b/python-dockering/tests/test_config_building.py
@@ -0,0 +1,150 @@
+# org.onap.dcae
+# ================================================================================
+# Copyright (c) 2017 AT&T Intellectual Property. All rights reserved.
+# ================================================================================
+# 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.
+# ============LICENSE_END=========================================================
+#
+# ECOMP is a trademark and service mark of AT&T Intellectual Property.
+
+from functools import partial
+import pytest
+import docker
+from dockering import config_building as doc
+from dockering.exceptions import DockerConstructionError
+
+
+# The docker-py library sneakily expects version to "know" that there is an
+# actual Docker API that you can connect with.
+DOCKER_API_VERSION = "1.24"
+create_host_config = partial(docker.utils.utils.create_host_config,
+ version=DOCKER_API_VERSION)
+
+def test_add_host_config_params_volumes():
+ hcp = doc.add_host_config_params_volumes()
+ hc = create_host_config(**hcp)
+ expected = { 'NetworkMode': 'default' }
+ assert expected == hc
+
+ volumes = [{"host": {"path": "some-path-host"},
+ "container": {"bind": "some-path-container", "mode": "ro"}}]
+ hcp = doc.add_host_config_params_volumes(volumes=volumes)
+ hc = create_host_config(**hcp)
+ expected = {'Binds': ['some-path-host:some-path-container:ro'], 'NetworkMode': 'default'}
+ assert expected == hc
+
+
+def test_add_host_config_params_ports():
+ ports = [ "22:22", "80:80" ]
+ hcp = doc.add_host_config_params_ports(ports=ports)
+ hc = create_host_config(**hcp)
+ expected = {'PortBindings': {'22/tcp': [{'HostPort': '22', 'HostIp': ''}],
+ '80/tcp': [{'HostPort': '80', 'HostIp': ''}]}, 'NetworkMode': 'default'}
+ assert expected == hc
+
+ hcp = doc.add_host_config_params_ports()
+ hc = create_host_config(**hcp)
+ expected = {'NetworkMode': 'default', 'PublishAllPorts': True}
+ assert expected == hc
+
+
+def test_add_host_config_params_dns():
+ docker_host = "192.168.1.1"
+ hcp = doc.add_host_config_params_dns(docker_host)
+ hc = create_host_config(**hcp)
+ expected = {'NetworkMode': 'default', 'DnsSearch': ['service.consul'],
+ 'Dns': ['192.168.1.1'], 'ExtraHosts': ['consul:192.168.1.1']}
+ assert expected == hc
+
+
+def test_create_envs_healthcheck():
+ endpoint = "/foo"
+ interval = "10s"
+ timeout = "1s"
+
+ docker_config = {
+ "healthcheck": {
+ "type": "http",
+ "endpoint": endpoint,
+ "interval": interval,
+ "timeout": timeout
+ }
+ }
+
+ expected = {
+ "SERVICE_CHECK_HTTP": endpoint,
+ "SERVICE_CHECK_INTERVAL": interval,
+ "SERVICE_CHECK_TIMEOUT": timeout
+ }
+
+ assert expected == doc.create_envs_healthcheck(docker_config)
+
+ docker_config["healthcheck"]["type"] = "https"
+ expected = {
+ "SERVICE_CHECK_HTTPS": endpoint,
+ "SERVICE_CHECK_INTERVAL": interval,
+ "SERVICE_CHECK_TIMEOUT": timeout
+ }
+
+ assert expected == doc.create_envs_healthcheck(docker_config)
+
+ # Good case for just script
+
+ script = "/bin/boo"
+ docker_config["healthcheck"]["type"] = "script"
+ docker_config["healthcheck"]["script"] = script
+ expected = {
+ "SERVICE_CHECK_SCRIPT": script,
+ "SERVICE_CHECK_INTERVAL": interval,
+ "SERVICE_CHECK_TIMEOUT": timeout
+ }
+
+ assert expected == doc.create_envs_healthcheck(docker_config)
+
+ # Good case for Docker script
+
+ script = "/bin/boo"
+ docker_config["healthcheck"]["type"] = "docker"
+ docker_config["healthcheck"]["script"] = script
+ expected = {
+ "SERVICE_CHECK_DOCKER_SCRIPT": script,
+ "SERVICE_CHECK_INTERVAL": interval,
+ "SERVICE_CHECK_TIMEOUT": timeout
+ }
+
+ assert expected == doc.create_envs_healthcheck(docker_config)
+
+ docker_config["healthcheck"]["type"] = None
+ with pytest.raises(DockerConstructionError):
+ doc.create_envs_healthcheck(docker_config)
+
+
+def test_create_envs():
+ service_component_name = "foo"
+ expected_env = { "HOSTNAME": service_component_name, "SERVICE_NAME": service_component_name,
+ "KEY_ONE": "value_z", "KEY_TWO": "value_y" }
+ env_one = { "KEY_ONE": "value_z" }
+ env_two = { "KEY_TWO": "value_y" }
+
+ assert expected_env == doc.create_envs(service_component_name, env_one, env_two)
+
+
+def test_parse_volumes_param():
+ volumes = [{ "host": { "path": "/var/run/docker.sock" },
+ "container": { "bind": "/tmp/docker.sock", "mode": "ro" } }]
+
+ expected = {'/var/run/docker.sock': {'bind': '/tmp/docker.sock', 'mode': 'ro'}}
+ actual = doc._parse_volumes_param(volumes)
+ assert actual == expected
+
+ assert None == doc._parse_volumes_param(None)
diff --git a/python-dockering/tests/test_core.py b/python-dockering/tests/test_core.py
new file mode 100644
index 0000000..e99dbd4
--- /dev/null
+++ b/python-dockering/tests/test_core.py
@@ -0,0 +1,71 @@
+# org.onap.dcae
+# ================================================================================
+# Copyright (c) 2017 AT&T Intellectual Property. All rights reserved.
+# ================================================================================
+# 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.
+# ============LICENSE_END=========================================================
+#
+# ECOMP is a trademark and service mark of AT&T Intellectual Property.
+
+import os
+from functools import partial
+import pytest
+import docker
+from dockering import core as doc
+from dockering.exceptions import DockerError, DockerConnectionError
+
+
+def test_create_client():
+ # Bad - Could not connect to docker engine
+
+ with pytest.raises(DockerConnectionError):
+ doc.create_client("fake", 2376, reauth=True)
+
+
+# TODO: Does pytest provide an env file?
+CONSUL_HOST = os.environ["CONSUL_HOST"]
+EXTERNAL_IP = os.environ["EXTERNAL_IP"]
+
+@pytest.mark.skip(reason="Need to automatically setup Docker engine and maybe Consul")
+def test_create_container():
+ client = doc.create_client("127.0.0.1", 2376)
+
+ scn = "unittest-registrator"
+ consul_host = CONSUL_HOST
+ # TODO: This may not work until we push the custom registrator into DockerHub
+ image_name = "registrator:latest"
+ envs = { "CONSUL_HOST": CONSUL_HOST,
+ "EXTERNAL_IP": EXTERNAL_IP }
+ volumes = {'/var/run/docker.sock': {'bind': '/tmp/docker.sock', 'mode': 'ro'}}
+
+ hcp = doc.add_host_config_params_volumes(volumes=volumes)
+ container = doc.create_container(client, image_name, scn, envs, hcp)
+
+ # Container is a dict with "Id". Check if container name matches scn.
+
+ try:
+ inspect_result = client.inspect_container(scn)
+ import pprint
+ pprint.pprint(inspect_result)
+
+ actual_mounts = inspect_result["Mounts"][0]
+ assert actual_mounts["Destination"] == volumes.values()[0]["bind"]
+ assert actual_mounts["Source"] == volumes.keys()[0]
+ except Exception as e:
+ raise e
+ finally:
+ # Execute teardown/cleanup
+ try:
+ doc.stop_then_remove_container(client, scn)
+ except:
+ print("Container removal failed")