Commit d93bc5c3 authored by Daniele Venzano's avatar Daniele Venzano
Browse files

Add unit tests for the new docker backend

parent 853a7a51
[DEFAULT]
use_tls: no
tls_cert: /mnt/test/cert.pem
tls_key: /mnt/test/key.pem
tls_ca: /mnt/test/ca.pem
[bf5]
docker_address: 192.168.47.5:2375
external_address: 192.168.47.5
use_tls: yes
[bf7]
docker_address: 192.168.47.7:2375
external_address: 192.168.47.7
use_tls: yes
[bf8]
docker_address: 192.168.47.8:2375
external_address: 192.168.47.8
use_tls: yes
[bf9]
docker_address: 192.168.47.9:2375
external_address: 192.168.47.9
use_tls: yes
[bf10]
docker_address: 192.168.47.10:2375
external_address: 192.168.47.10
use_tls: yes
[bf12]
docker_address: 192.168.47.12:2375
external_address: 192.168.47.12
use_tls: yes
[bf13]
docker_address: 192.168.47.13:2375
external_address: 192.168.47.13
use_tls: yes
[bf14]
docker_address: 192.168.47.14:2375
external_address: 192.168.47.14
use_tls: yes
[bf15]
docker_address: 192.168.47.15:2375
external_address: 192.168.47.15
use_tls: yes
[bf16]
docker_address: 192.168.47.16:2375
external_address: 192.168.47.16
use_tls: yes
[bf17]
docker_address: 192.168.47.17:2375
external_address: 192.168.47.17
use_tls: yes
[bf18]
docker_address: 192.168.47.18:2375
external_address: 192.168.47.18
use_tls: yes
[bf19]
docker_address: 192.168.47.19:2375
external_address: 192.168.47.19
use_tls: yes
[bf20]
docker_address: 192.168.47.20:2375
external_address: 192.168.47.20
use_tls: yes
[bf21]
docker_address: 192.168.47.21:2375
external_address: 192.168.47.21
use_tls: yes
[bf22]
docker_address: 192.168.47.22:2375
external_address: 192.168.47.22
use_tls: yes
...@@ -43,13 +43,18 @@ except AttributeError: ...@@ -43,13 +43,18 @@ except AttributeError:
class DockerClient: class DockerClient:
"""The client class that wraps the Docker API.""" """The client class that wraps the Docker API."""
def __init__(self, docker_config: DockerHostConfig) -> None: def __init__(self, docker_config: DockerHostConfig, mock_client=None) -> None:
self.name = docker_config.name self.name = docker_config.name
if not docker_config.tls: if not docker_config.tls:
tls = None tls = None
else: else:
tls = docker.tls.TLSConfig(client_cert=(docker_config.tls_cert, docker_config.tls_key), verify=docker_config.tls_ca) tls = docker.tls.TLSConfig(client_cert=(docker_config.tls_cert, docker_config.tls_key), verify=docker_config.tls_ca)
# Simplify testing
if mock_client is not None:
self.cli = mock_client
return
try: try:
self.cli = docker.DockerClient(base_url=docker_config.address, version="auto", tls=tls) self.cli = docker.DockerClient(base_url=docker_config.address, version="auto", tls=tls)
except docker.errors.DockerException as e: except docker.errors.DockerException as e:
...@@ -173,7 +178,7 @@ class DockerClient: ...@@ -173,7 +178,7 @@ class DockerClient:
info["running"] = False info["running"] = False
info['ports'] = {} info['ports'] = {}
if container.attrs['NetworkSettings']['Ports'] is not None: if 'Ports' in container.attrs['NetworkSettings'] and container.attrs['NetworkSettings']['Ports'] is not None:
for port in container.attrs['NetworkSettings']['Ports']: for port in container.attrs['NetworkSettings']['Ports']:
if container.attrs['NetworkSettings']['Ports'][port] is not None: if container.attrs['NetworkSettings']['Ports'][port] is not None:
info['ports'][port] = container.attrs['NetworkSettings']['Ports'][port][0]['HostPort'] info['ports'][port] = container.attrs['NetworkSettings']['Ports'][port][0]['HostPort']
...@@ -190,7 +195,7 @@ class DockerClient: ...@@ -190,7 +195,7 @@ class DockerClient:
def inspect_container(self, docker_id: str) -> Dict[str, Any]: def inspect_container(self, docker_id: str) -> Dict[str, Any]:
"""Retrieve information about a running container.""" """Retrieve information about a running container."""
try: try:
cont = self.cli.container.get(docker_id) cont = self.cli.containers.get(docker_id)
except Exception as e: except Exception as e:
raise ZoeException(str(e)) raise ZoeException(str(e))
return self._container_summary(cont) return self._container_summary(cont)
......
...@@ -37,7 +37,7 @@ class DockerEngineBackend(zoe_master.backends.base.BaseBackend): ...@@ -37,7 +37,7 @@ class DockerEngineBackend(zoe_master.backends.base.BaseBackend):
"""Zoe backend implementation for old-style stand-alone Docker Swarm.""" """Zoe backend implementation for old-style stand-alone Docker Swarm."""
def __init__(self, opts): def __init__(self, opts):
super().__init__(opts) super().__init__(opts)
self.docker_config = DockerConfig().read_config() self.docker_config = DockerConfig(get_conf().backend_docker_config_file).read_config()
def _get_config(self, host) -> DockerHostConfig: def _get_config(self, host) -> DockerHostConfig:
for conf in self.docker_config: for conf in self.docker_config:
......
...@@ -19,8 +19,6 @@ import configparser ...@@ -19,8 +19,6 @@ import configparser
import logging import logging
from typing import List from typing import List
from zoe_lib.config import get_conf
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -39,8 +37,8 @@ class DockerHostConfig: ...@@ -39,8 +37,8 @@ class DockerHostConfig:
class DockerConfig: class DockerConfig:
"""A class that holds the configuration for the Docker Engine backend.""" """A class that holds the configuration for the Docker Engine backend."""
def __init__(self): def __init__(self, config_file):
self.conffile = get_conf().backend_docker_config_file self.conffile = config_file
def read_config(self) -> List[DockerHostConfig]: def read_config(self) -> List[DockerHostConfig]:
"""Parse the configuration file.""" """Parse the configuration file."""
......
# Copyright (c) 2017, Daniele Venzano
#
# 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.
"""Unit tests"""
import pytest
from zoe_master.backends.docker import api_client
from zoe_master.backends.docker.config import DockerHostConfig
from zoe_master.exceptions import ZoeException
class MockDocker:
"""A mock object for the official docker client."""
def __init__(self):
self.containers = MockContainerModel()
def info(self):
"""The info method."""
return {
"ID": "7TRN:IPZB:QYBB:VPBQ:UMPP:KARE:6ZNR:XE6T:7EWV:PKF4:ZOJD:TPYS",
"Containers": 14,
"ContainersRunning": 3,
"ContainersPaused": 1,
"ContainersStopped": 10,
"Images": 508,
"Driver": "overlay2",
"DriverStatus": [
[
"Backing Filesystem",
"extfs"
],
[
"Supports d_type",
"true"
],
[
"Native Overlay Diff",
"true"
]
],
"DockerRootDir": "/var/lib/docker",
"SystemStatus": [],
"Plugins": {
"Volume": [
"local"
],
"Network": [
"bridge",
"host",
"ipvlan",
"macvlan",
"null",
"overlay"
],
"Authorization": [
"img-authz-plugin",
"hbm"
],
"Log": [
"awslogs",
"fluentd",
"gcplogs",
"gelf",
"journald",
"json-file",
"logentries",
"splunk",
"syslog"
]
},
"MemoryLimit": True,
"SwapLimit": True,
"KernelMemory": True,
"CpuCfsPeriod": True,
"CpuCfsQuota": True,
"CPUShares": True,
"CPUSet": True,
"OomKillDisable": True,
"IPv4Forwarding": True,
"BridgeNfIptables": True,
"BridgeNfIp6tables": True,
"Debug": False,
"NFd": 64,
"NGoroutines": 174,
"SystemTime": "2017-08-08T20:28:29.06202363Z",
"LoggingDriver": "string",
"CgroupDriver": "cgroupfs",
"NEventsListener": 30,
"KernelVersion": "4.9.38-moby",
"OperatingSystem": "Alpine Linux v3.5",
"OSType": "linux",
"Architecture": "x86_64",
"NCPU": 4,
"MemTotal": 2095882240,
"IndexServerAddress": "https://index.docker.io/v1/",
"RegistryConfig": {
"InsecureRegistryCIDRs": [
"::1/128",
"127.0.0.0/8"
],
"IndexConfigs": {
"127.0.0.1:5000": {
"Name": "127.0.0.1:5000",
"Mirrors": [],
"Secure": False,
"Official": False
},
"[2001:db8:a0b:12f0::1]:80": {
"Name": "[2001:db8:a0b:12f0::1]:80",
"Mirrors": [],
"Secure": False,
"Official": False
},
"docker.io": {
"Name": "docker.io",
"Mirrors": [
"https://hub-mirror.corp.example.com:5000/"
],
"Secure": True,
"Official": True
}
},
"Mirrors": []
},
"GenericResources": [],
"HttpProxy": "http://user:pass@proxy.corp.example.com:8080",
"HttpsProxy": "https://user:pass@proxy.corp.example.com:4443",
"NoProxy": "*.local, 169.254/16",
"Name": "node5.corp.example.com",
"Labels": [
"storage=ssd",
"production"
],
"ExperimentalBuild": False,
"ServerVersion": "17.06.0-ce",
"ClusterStore": "consul://consul.corp.example.com:8600/some/path",
"ClusterAdvertise": "node5.corp.example.com:8000",
"Runtimes": {
"runc": {
"path": "docker-runc"
}
},
"DefaultRuntime": "runc",
"Swarm": {},
"LiveRestoreEnabled": False,
"Isolation": "default",
"InitBinary": "docker-init"
}
class MockContainerModel:
"""A mock object fot the docker container model."""
def get(self, docker_id):
"""The get method"""
return MockContainer(docker_id)
class MockContainer:
"""A mock container object"""
def __init__(self, docker_id):
self.id = docker_id
self.name = 'mock-container'
self.attrs = {
"AppArmorProfile": "",
"Args": [
"-c",
"exit 9"
],
"Config": {
"AttachStderr": True,
"AttachStdin": False,
"AttachStdout": True,
"Cmd": [
"/bin/sh",
"-c",
"exit 9"
],
"Domainname": "",
"Env": [
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
],
"Hostname": "ba033ac44011",
"Image": "ubuntu",
"Labels": {
"com.example.vendor": "Acme",
"com.example.license": "GPL",
"com.example.version": "1.0"
},
"MacAddress": "",
"NetworkDisabled": False,
"OpenStdin": False,
"StdinOnce": False,
"Tty": False,
"User": "",
"Volumes": {
"/volumes/data": {}
},
"WorkingDir": "",
"StopSignal": "SIGTERM",
"StopTimeout": 10
},
"Created": "2015-01-06T15:47:31.485331387Z",
"Driver": "devicemapper",
"HostConfig": {
"MaximumIOps": 0,
"MaximumIOBps": 0,
"BlkioWeight": 0,
"BlkioWeightDevice": [
{}
],
"BlkioDeviceReadBps": [
{}
],
"BlkioDeviceWriteBps": [
{}
],
"BlkioDeviceReadIOps": [
{}
],
"BlkioDeviceWriteIOps": [
{}
],
"ContainerIDFile": "",
"CpusetCpus": "",
"CpusetMems": "",
"CpuPercent": 0,
"CpuQuota": 100000,
"CpuPeriod": 100000,
"CpuRealtimePeriod": 1000000,
"CpuRealtimeRuntime": 10000,
"Devices": [],
"IpcMode": "",
"LxcConf": [],
"Memory": 0,
"MemorySwap": 0,
"MemoryReservation": 0,
"KernelMemory": 0,
"OomKillDisable": False,
"OomScoreAdj": 500,
"NetworkMode": "bridge",
"PidMode": "",
"PortBindings": {},
"Privileged": False,
"ReadonlyRootfs": False,
"PublishAllPorts": False,
"RestartPolicy": {
"MaximumRetryCount": 2,
"Name": "on-failure"
},
"LogConfig": {
"Type": "json-file"
},
"Sysctls": {
"net.ipv4.ip_forward": "1"
},
"Ulimits": [
{}
],
"VolumeDriver": "",
"ShmSize": 67108864
},
"HostnamePath": "/var/lib/docker/containers/ba033ac4401106a3b513bc9d639eee123ad78ca3616b921167cd74b20e25ed39/hostname",
"HostsPath": "/var/lib/docker/containers/ba033ac4401106a3b513bc9d639eee123ad78ca3616b921167cd74b20e25ed39/hosts",
"LogPath": "/var/lib/docker/containers/1eb5fabf5a03807136561b3c00adcd2992b535d624d5e18b6cdc6a6844d9767b/1eb5fabf5a03807136561b3c00adcd2992b535d624d5e18b6cdc6a6844d9767b-json.log",
"Id": "ba033ac4401106a3b513bc9d639eee123ad78ca3616b921167cd74b20e25ed39",
"Image": "04c5d3b7b0656168630d3ba35d8889bd0e9caafcaeb3004d2bfbc47e7c5d35d2",
"MountLabel": "",
"Name": "/boring_euclid",
"NetworkSettings": {
"Bridge": "",
"SandboxID": "",
"HairpinMode": False,
"LinkLocalIPv6Address": "",
"LinkLocalIPv6PrefixLen": 0,
"SandboxKey": "",
"EndpointID": "",
"Gateway": "",
"GlobalIPv6Address": "",
"GlobalIPv6PrefixLen": 0,
"IPAddress": "",
"IPPrefixLen": 0,
"IPv6Gateway": "",
"MacAddress": "",
"Networks": {
"bridge": {
"NetworkID": "7ea29fc1412292a2d7bba362f9253545fecdfa8ce9a6e37dd10ba8bee7129812",
"EndpointID": "7587b82f0dada3656fda26588aee72630c6fab1536d36e394b2bfbcf898c971d",
"Gateway": "172.17.0.1",
"IPAddress": "172.17.0.2",
"IPPrefixLen": 16,
"IPv6Gateway": "",
"GlobalIPv6Address": "",
"GlobalIPv6PrefixLen": 0,
"MacAddress": "02:42:ac:12:00:02"
}
}
},
"Path": "/bin/sh",
"ProcessLabel": "",
"ResolvConfPath": "/var/lib/docker/containers/ba033ac4401106a3b513bc9d639eee123ad78ca3616b921167cd74b20e25ed39/resolv.conf",
"RestartCount": 1,
"State": {
"Error": "",
"ExitCode": 9,
"FinishedAt": "2015-01-06T15:47:32.080254511Z",
"OOMKilled": False,
"Dead": False,
"Paused": False,
"Pid": 0,
"Restarting": False,
"Running": True,
"StartedAt": "2015-01-06T15:47:32.072697474Z",
"Status": "running"
},
"Mounts": [
{
"Name": "fac362...80535",
"Source": "/data",
"Destination": "/data",
"Driver": "local",
"Mode": "ro,Z",
"RW": False,
"Propagation": ""
}
]
}
self.status = 'running'
def stop(self, timeout=5):
"""Stop method"""
return
def remove(self, force=False):
"""Remove method"""
return
class TestDockerEngineApiClient:
"""Docker low-level wrapper testing."""
@pytest.fixture
def docker_client(self):
"""The mock Docker client object."""
return MockDocker()
def test_init_fail(self):
"""Test that initialization fails with a bad address."""
dhc = DockerHostConfig()
dhc.name = 'test'
dhc.address = '999.999.999.999:9999'
with pytest.raises(ZoeException):
api_client.DockerClient(dhc)
def test_info(self, docker_client):
"""Test that the info method works."""
dhc = DockerHostConfig()
dhc.name = 'test'
mock_client = docker_client
cli = api_client.DockerClient(dhc, mock_client)
assert cli.info() is not None
def test_inspect_container(self, docker_client):
"""Test the inspect container parsing logic."""
dhc = DockerHostConfig()
dhc.name = 'test'
mock_client = docker_client
cli = api_client.DockerClient(dhc, mock_client)
assert cli.inspect_container('test') is not None
def test_terminate_container(self, docker_client):
"""Test the terminate container method."""
dhc = DockerHostConfig()
dhc.name = 'test'
mock_client = docker_client
cli = api_client.DockerClient(dhc, mock_client)
cli.terminate_container('test')
cli.terminate_container('test', delete=True)
# Copyright (c) 2017, Daniele Venzano
#
# 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.
"""Unit tests"""
from zoe_master.backends.docker import config
class TestDockerEngineBackendConfig:
"""Docker configuration parsing tests."""
def test_new_docker_host_config(self):
"""Test the DockerHostConfig object."""
config.DockerHostConfig()
def test_parsing_config_file(self):
"""Test Docker backend config parsing."""
hosts = config.DockerConfig(config_file='tests/sample_docker.conf').read_config()
assert len(hosts) == 16
...@@ -46,7 +46,7 @@ class DockerStateSynchronizer(threading.Thread): ...@@ -46,7 +46,7 @@ class DockerStateSynchronizer(threading.Thread):