Merge "Refactor DockerWorker into ContainerWorker"
This commit is contained in:
commit
308089bb64
@ -293,7 +293,9 @@ def generate_module():
|
||||
'shareable']),
|
||||
cap_add=dict(required=False, type='list', default=list()),
|
||||
security_opt=dict(required=False, type='list', default=list()),
|
||||
pid_mode=dict(required=False, type='str', choices=['host', '']),
|
||||
pid_mode=dict(required=False, type='str', choices=['',
|
||||
'host',
|
||||
'private']),
|
||||
cgroupns_mode=dict(required=False, type='str',
|
||||
choices=['private', 'host']),
|
||||
privileged=dict(required=False, type='bool', default=False),
|
||||
|
499
ansible/module_utils/kolla_container_worker.py
Normal file
499
ansible/module_utils/kolla_container_worker.py
Normal file
@ -0,0 +1,499 @@
|
||||
# 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.
|
||||
|
||||
from abc import ABC
|
||||
from abc import abstractmethod
|
||||
import shlex
|
||||
|
||||
from ansible.module_utils.kolla_systemd_worker import SystemdWorker
|
||||
|
||||
COMPARE_CONFIG_CMD = ['/usr/local/bin/kolla_set_configs', '--check']
|
||||
|
||||
|
||||
class ContainerWorker(ABC):
|
||||
def __init__(self, module):
|
||||
self.module = module
|
||||
self.params = self.module.params
|
||||
self.changed = False
|
||||
# Use this to store arguments to pass to exit_json().
|
||||
self.result = {}
|
||||
self._cgroupns_mode_supported = True
|
||||
|
||||
self.systemd = SystemdWorker(self.params)
|
||||
|
||||
# NOTE(mgoddard): The names used by Docker are inconsisent between
|
||||
# configuration of a container's resources and the resources in
|
||||
# container_info['HostConfig']. This provides a mapping between the
|
||||
# two.
|
||||
self.dimension_map = {
|
||||
'mem_limit': 'Memory', 'mem_reservation': 'MemoryReservation',
|
||||
'memswap_limit': 'MemorySwap', 'cpu_period': 'CpuPeriod',
|
||||
'cpu_quota': 'CpuQuota', 'cpu_shares': 'CpuShares',
|
||||
'cpuset_cpus': 'CpusetCpus', 'cpuset_mems': 'CpusetMems',
|
||||
'kernel_memory': 'KernelMemory', 'blkio_weight': 'BlkioWeight',
|
||||
'ulimits': 'Ulimits'}
|
||||
|
||||
@abstractmethod
|
||||
def check_image(self):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_container_info(self):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def check_container(self):
|
||||
pass
|
||||
|
||||
def compare_container(self):
|
||||
container = self.check_container()
|
||||
if (not container or
|
||||
self.check_container_differs() or
|
||||
self.compare_config() or
|
||||
self.systemd.check_unit_change()):
|
||||
self.changed = True
|
||||
return self.changed
|
||||
|
||||
def check_container_differs(self):
|
||||
container_info = self.get_container_info()
|
||||
if not container_info:
|
||||
return True
|
||||
|
||||
return (
|
||||
self.compare_cap_add(container_info) or
|
||||
self.compare_security_opt(container_info) or
|
||||
self.compare_image(container_info) or
|
||||
self.compare_ipc_mode(container_info) or
|
||||
self.compare_labels(container_info) or
|
||||
self.compare_privileged(container_info) or
|
||||
self.compare_pid_mode(container_info) or
|
||||
self.compare_cgroupns_mode(container_info) or
|
||||
self.compare_tmpfs(container_info) or
|
||||
self.compare_volumes(container_info) or
|
||||
self.compare_volumes_from(container_info) or
|
||||
self.compare_environment(container_info) or
|
||||
self.compare_container_state(container_info) or
|
||||
self.compare_dimensions(container_info) or
|
||||
self.compare_command(container_info) or
|
||||
self.compare_healthcheck(container_info)
|
||||
)
|
||||
|
||||
def compare_ipc_mode(self, container_info):
|
||||
new_ipc_mode = self.params.get('ipc_mode')
|
||||
current_ipc_mode = container_info['HostConfig'].get('IpcMode')
|
||||
if not current_ipc_mode:
|
||||
current_ipc_mode = None
|
||||
|
||||
# only check IPC mode if it is specified
|
||||
if new_ipc_mode is not None and new_ipc_mode != current_ipc_mode:
|
||||
return True
|
||||
return False
|
||||
|
||||
def compare_cap_add(self, container_info):
|
||||
new_cap_add = self.params.get('cap_add', list())
|
||||
try:
|
||||
current_cap_add = container_info['HostConfig'].get('CapAdd', None)
|
||||
except KeyError:
|
||||
current_cap_add = None
|
||||
except TypeError:
|
||||
current_cap_add = None
|
||||
|
||||
if not current_cap_add:
|
||||
current_cap_add = list()
|
||||
if set(new_cap_add).symmetric_difference(set(current_cap_add)):
|
||||
return True
|
||||
|
||||
def compare_security_opt(self, container_info):
|
||||
ipc_mode = self.params.get('ipc_mode')
|
||||
pid_mode = self.params.get('pid_mode')
|
||||
privileged = self.params.get('privileged', False)
|
||||
# NOTE(jeffrey4l) security opt is disabled when using host ipc mode or
|
||||
# host pid mode or privileged. So no need to compare security opts
|
||||
if ipc_mode == 'host' or pid_mode == 'host' or privileged:
|
||||
return False
|
||||
new_sec_opt = self.params.get('security_opt', list())
|
||||
try:
|
||||
current_sec_opt = container_info['HostConfig'].get('SecurityOpt',
|
||||
list())
|
||||
except KeyError:
|
||||
current_sec_opt = None
|
||||
except TypeError:
|
||||
current_sec_opt = None
|
||||
|
||||
if not current_sec_opt:
|
||||
current_sec_opt = list()
|
||||
if set(new_sec_opt).symmetric_difference(set(current_sec_opt)):
|
||||
return True
|
||||
|
||||
@abstractmethod
|
||||
def compare_pid_mode(self, container_info):
|
||||
pass
|
||||
|
||||
def compare_cgroupns_mode(self, container_info):
|
||||
if not self._cgroupns_mode_supported:
|
||||
return False
|
||||
new_cgroupns_mode = self.params.get('cgroupns_mode')
|
||||
if new_cgroupns_mode is None:
|
||||
# means we don't care what it is
|
||||
return False
|
||||
current_cgroupns_mode = (container_info['HostConfig']
|
||||
.get('CgroupnsMode')) or \
|
||||
(container_info['HostConfig']
|
||||
.get('CgroupMode'))
|
||||
if current_cgroupns_mode in ('', None):
|
||||
# means the container was created on Docker pre-20.10
|
||||
# it behaves like 'host'
|
||||
current_cgroupns_mode = 'host'
|
||||
return new_cgroupns_mode != current_cgroupns_mode
|
||||
|
||||
def compare_privileged(self, container_info):
|
||||
new_privileged = self.params.get('privileged')
|
||||
current_privileged = container_info['HostConfig']['Privileged']
|
||||
if new_privileged != current_privileged:
|
||||
return True
|
||||
|
||||
@abstractmethod
|
||||
def compare_image(self, container_info=None):
|
||||
pass
|
||||
|
||||
def compare_labels(self, container_info):
|
||||
new_labels = self.params.get('labels')
|
||||
current_labels = container_info['Config'].get('Labels', dict())
|
||||
image_labels = self.check_image().get('Labels', dict())
|
||||
for k, v in image_labels.items():
|
||||
if k in new_labels:
|
||||
if v != new_labels[k]:
|
||||
return True
|
||||
else:
|
||||
del current_labels[k]
|
||||
|
||||
if new_labels != current_labels:
|
||||
return True
|
||||
|
||||
def compare_tmpfs(self, container_info):
|
||||
new_tmpfs = self.generate_tmpfs()
|
||||
current_tmpfs = container_info['HostConfig'].get('Tmpfs')
|
||||
if not new_tmpfs:
|
||||
new_tmpfs = []
|
||||
if not current_tmpfs:
|
||||
current_tmpfs = []
|
||||
|
||||
if set(current_tmpfs).symmetric_difference(set(new_tmpfs)):
|
||||
return True
|
||||
|
||||
def compare_volumes_from(self, container_info):
|
||||
new_vols_from = self.params.get('volumes_from')
|
||||
current_vols_from = container_info['HostConfig'].get('VolumesFrom')
|
||||
if not new_vols_from:
|
||||
new_vols_from = list()
|
||||
if not current_vols_from:
|
||||
current_vols_from = list()
|
||||
|
||||
if set(current_vols_from).symmetric_difference(set(new_vols_from)):
|
||||
return True
|
||||
|
||||
@abstractmethod
|
||||
def compare_volumes(self, container_info):
|
||||
pass
|
||||
|
||||
def compare_dimensions(self, container_info):
|
||||
new_dimensions = self.params.get('dimensions')
|
||||
|
||||
if not self._dimensions_kernel_memory_removed:
|
||||
self.dimension_map['kernel_memory'] = 'KernelMemory'
|
||||
|
||||
unsupported = set(new_dimensions.keys()) - \
|
||||
set(self.dimension_map.keys())
|
||||
if unsupported:
|
||||
self.module.exit_json(
|
||||
failed=True, msg=repr("Unsupported dimensions"),
|
||||
unsupported_dimensions=unsupported)
|
||||
current_dimensions = container_info['HostConfig']
|
||||
for key1, key2 in self.dimension_map.items():
|
||||
# NOTE(mgoddard): If a resource has been explicitly requested,
|
||||
# check for a match. Otherwise, ensure it is set to the default.
|
||||
if key1 in new_dimensions:
|
||||
if key1 == 'ulimits':
|
||||
if self.compare_ulimits(new_dimensions[key1],
|
||||
current_dimensions[key2]):
|
||||
return True
|
||||
elif new_dimensions[key1] != current_dimensions[key2]:
|
||||
return True
|
||||
elif current_dimensions[key2]:
|
||||
# The default values of all currently supported resources are
|
||||
# '' or 0 - both falsy.
|
||||
return True
|
||||
|
||||
def compare_environment(self, container_info):
|
||||
if self.params.get('environment'):
|
||||
current_env = dict()
|
||||
for kv in container_info['Config'].get('Env', list()):
|
||||
k, v = kv.split('=', 1)
|
||||
current_env.update({k: v})
|
||||
|
||||
for k, v in self.params.get('environment').items():
|
||||
if k not in current_env:
|
||||
return True
|
||||
if current_env[k] != v:
|
||||
return True
|
||||
|
||||
def compare_container_state(self, container_info):
|
||||
new_state = self.params.get('state')
|
||||
current_state = container_info['State'].get('Status')
|
||||
|
||||
if new_state == "started" and current_state == "running":
|
||||
return False
|
||||
if new_state != current_state:
|
||||
return True
|
||||
|
||||
def compare_ulimits(self, new_ulimits, current_ulimits):
|
||||
# The new_ulimits is dict, we need make it to a list of Ulimit
|
||||
# instance.
|
||||
new_ulimits = self.build_ulimits(new_ulimits)
|
||||
|
||||
def key(ulimit):
|
||||
return ulimit['Name']
|
||||
|
||||
if current_ulimits is None:
|
||||
current_ulimits = []
|
||||
return sorted(new_ulimits, key=key) != sorted(current_ulimits, key=key)
|
||||
|
||||
def compare_command(self, container_info):
|
||||
new_command = self.params.get('command')
|
||||
if new_command is not None:
|
||||
new_command_split = shlex.split(new_command)
|
||||
new_path = new_command_split[0]
|
||||
new_args = new_command_split[1:]
|
||||
if (new_path != container_info['Path'] or
|
||||
new_args != container_info['Args']):
|
||||
return True
|
||||
|
||||
def compare_healthcheck(self, container_info):
|
||||
new_healthcheck = self.parse_healthcheck(
|
||||
self.params.get('healthcheck'))
|
||||
current_healthcheck = container_info['Config'].get('Healthcheck')
|
||||
|
||||
healthcheck_map = {
|
||||
'test': 'Test',
|
||||
'retries': 'Retries',
|
||||
'interval': 'Interval',
|
||||
'start_period': 'StartPeriod',
|
||||
'timeout': 'Timeout'}
|
||||
|
||||
if new_healthcheck:
|
||||
new_healthcheck = new_healthcheck['healthcheck']
|
||||
if current_healthcheck:
|
||||
new_healthcheck = dict((healthcheck_map.get(k, k), v)
|
||||
for (k, v) in new_healthcheck.items())
|
||||
return new_healthcheck != current_healthcheck
|
||||
else:
|
||||
return True
|
||||
else:
|
||||
if current_healthcheck:
|
||||
return True
|
||||
|
||||
def parse_image(self):
|
||||
full_image = self.params.get('image')
|
||||
|
||||
if '/' in full_image:
|
||||
registry, image = full_image.split('/', 1)
|
||||
else:
|
||||
image = full_image
|
||||
|
||||
if ':' in image:
|
||||
return full_image.rsplit(':', 1)
|
||||
else:
|
||||
return full_image, 'latest'
|
||||
|
||||
@abstractmethod
|
||||
def pull_image(self):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def remove_container(self):
|
||||
pass
|
||||
|
||||
def generate_tmpfs(self):
|
||||
tmpfs = self.params.get('tmpfs')
|
||||
if tmpfs:
|
||||
# NOTE(mgoddard): Filter out any empty strings.
|
||||
tmpfs = [t for t in tmpfs if t]
|
||||
return tmpfs
|
||||
|
||||
def generate_volumes(self, binds=None):
|
||||
if not binds:
|
||||
volumes = self.params.get('volumes') or self.params.get('volume')
|
||||
else:
|
||||
volumes = binds
|
||||
|
||||
if not volumes:
|
||||
return None, None
|
||||
|
||||
vol_list = list()
|
||||
vol_dict = dict()
|
||||
|
||||
for vol in volumes:
|
||||
if len(vol) == 0:
|
||||
continue
|
||||
|
||||
if ':' not in vol:
|
||||
vol_list.append(vol)
|
||||
continue
|
||||
|
||||
split_vol = vol.split(':')
|
||||
|
||||
if (len(split_vol) == 2 and
|
||||
('/' not in split_vol[0] or '/' in split_vol[1])):
|
||||
split_vol.append('rw')
|
||||
|
||||
vol_list.append(split_vol[1])
|
||||
vol_dict.update({
|
||||
split_vol[0]: {
|
||||
'bind': split_vol[1],
|
||||
'mode': split_vol[2]
|
||||
}
|
||||
})
|
||||
|
||||
return vol_list, vol_dict
|
||||
|
||||
@abstractmethod
|
||||
def build_ulimits(self, ulimits):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def create_container(self):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def recreate_or_restart_container(self):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def start_container(self):
|
||||
pass
|
||||
|
||||
def get_container_env(self):
|
||||
name = self.params.get('name')
|
||||
info = self.get_container_info()
|
||||
if not info:
|
||||
self.module.fail_json(msg="No such container: {}".format(name))
|
||||
else:
|
||||
envs = dict()
|
||||
for env in info['Config']['Env']:
|
||||
if '=' in env:
|
||||
key, value = env.split('=', 1)
|
||||
else:
|
||||
key, value = env, ''
|
||||
envs[key] = value
|
||||
|
||||
self.module.exit_json(**envs)
|
||||
|
||||
def get_container_state(self):
|
||||
name = self.params.get('name')
|
||||
info = self.get_container_info()
|
||||
if not info:
|
||||
self.module.fail_json(msg="No such container: {}".format(name))
|
||||
else:
|
||||
self.module.exit_json(**info['State'])
|
||||
|
||||
def parse_healthcheck(self, healthcheck):
|
||||
if not healthcheck:
|
||||
return None
|
||||
|
||||
result = dict(healthcheck={})
|
||||
|
||||
# All supported healthcheck parameters
|
||||
supported = set(['test', 'interval', 'timeout', 'start_period',
|
||||
'retries'])
|
||||
unsupported = set(healthcheck) - supported
|
||||
missing = supported - set(healthcheck)
|
||||
duration_options = set(['interval', 'timeout', 'start_period'])
|
||||
|
||||
if unsupported:
|
||||
self.module.exit_json(failed=True,
|
||||
msg=repr("Unsupported healthcheck options"),
|
||||
unsupported_healthcheck=unsupported)
|
||||
|
||||
if missing:
|
||||
self.module.exit_json(failed=True,
|
||||
msg=repr("Missing healthcheck option"),
|
||||
missing_healthcheck=missing)
|
||||
|
||||
for key in healthcheck:
|
||||
value = healthcheck.get(key)
|
||||
if key in duration_options:
|
||||
try:
|
||||
result['healthcheck'][key] = int(value) * 1000000000
|
||||
except TypeError:
|
||||
raise TypeError(
|
||||
'Cannot parse healthcheck "{0}". '
|
||||
'Expected an integer, got "{1}".'
|
||||
.format(value, type(value).__name__)
|
||||
)
|
||||
except ValueError:
|
||||
raise ValueError(
|
||||
'Cannot parse healthcheck "{0}". '
|
||||
'Expected an integer, got "{1}".'
|
||||
.format(value, type(value).__name__)
|
||||
)
|
||||
else:
|
||||
if key == 'test':
|
||||
# If the user explicitly disables the healthcheck,
|
||||
# return None as the healthcheck object
|
||||
if value in (['NONE'], 'NONE'):
|
||||
return None
|
||||
else:
|
||||
if isinstance(value, (tuple, list)):
|
||||
result['healthcheck'][key] = \
|
||||
[str(e) for e in value]
|
||||
else:
|
||||
result['healthcheck'][key] = \
|
||||
['CMD-SHELL', str(value)]
|
||||
elif key == 'retries':
|
||||
try:
|
||||
result['healthcheck'][key] = int(value)
|
||||
except ValueError:
|
||||
raise ValueError(
|
||||
'Cannot parse healthcheck number of retries.'
|
||||
'Expected an integer, got "{0}".'
|
||||
.format(type(value))
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
@abstractmethod
|
||||
def stop_container(self):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def stop_and_remove_container(self):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def restart_container(self):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def create_volume(self):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def remove_volume(self):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def remove_image(self):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def ensure_image(self):
|
||||
pass
|
@ -17,29 +17,21 @@
|
||||
import docker
|
||||
import json
|
||||
import os
|
||||
import shlex
|
||||
|
||||
from ansible.module_utils.kolla_systemd_worker import SystemdWorker
|
||||
from ansible.module_utils.kolla_container_worker import COMPARE_CONFIG_CMD
|
||||
from ansible.module_utils.kolla_container_worker import ContainerWorker
|
||||
|
||||
from distutils.version import StrictVersion
|
||||
|
||||
COMPARE_CONFIG_CMD = ['/usr/local/bin/kolla_set_configs', '--check']
|
||||
|
||||
|
||||
def get_docker_client():
|
||||
return docker.APIClient
|
||||
|
||||
|
||||
class DockerWorker(object):
|
||||
class DockerWorker(ContainerWorker):
|
||||
|
||||
def __init__(self, module):
|
||||
self.module = module
|
||||
self.params = self.module.params
|
||||
self.changed = False
|
||||
# Use this to store arguments to pass to exit_json().
|
||||
self.result = {}
|
||||
|
||||
# TLS not fully implemented
|
||||
# tls_config = self.generate_tls()
|
||||
super().__init__(module)
|
||||
|
||||
options = {
|
||||
'version': self.params.get('api_version'),
|
||||
@ -53,7 +45,8 @@ class DockerWorker(object):
|
||||
self._dimensions_kernel_memory_removed = (
|
||||
StrictVersion(self.dc._version) >= StrictVersion('1.42'))
|
||||
|
||||
self.systemd = SystemdWorker(self.params)
|
||||
if self._dimensions_kernel_memory_removed:
|
||||
self.dimension_map.pop('kernel_memory', None)
|
||||
|
||||
def generate_tls(self):
|
||||
tls = {'verify': self.params.get('tls_verify')}
|
||||
@ -111,72 +104,6 @@ class DockerWorker(object):
|
||||
return None
|
||||
return self.dc.inspect_container(self.params.get('name'))
|
||||
|
||||
def compare_container(self):
|
||||
container = self.check_container()
|
||||
if (not container or
|
||||
self.check_container_differs() or
|
||||
self.compare_config() or
|
||||
self.systemd.check_unit_change()):
|
||||
self.changed = True
|
||||
return self.changed
|
||||
|
||||
def check_container_differs(self):
|
||||
container_info = self.get_container_info()
|
||||
return (
|
||||
self.compare_cap_add(container_info) or
|
||||
self.compare_security_opt(container_info) or
|
||||
self.compare_image(container_info) or
|
||||
self.compare_ipc_mode(container_info) or
|
||||
self.compare_labels(container_info) or
|
||||
self.compare_privileged(container_info) or
|
||||
self.compare_pid_mode(container_info) or
|
||||
self.compare_cgroupns_mode(container_info) or
|
||||
self.compare_tmpfs(container_info) or
|
||||
self.compare_volumes(container_info) or
|
||||
self.compare_volumes_from(container_info) or
|
||||
self.compare_environment(container_info) or
|
||||
self.compare_container_state(container_info) or
|
||||
self.compare_dimensions(container_info) or
|
||||
self.compare_command(container_info) or
|
||||
self.compare_healthcheck(container_info)
|
||||
)
|
||||
|
||||
def compare_ipc_mode(self, container_info):
|
||||
new_ipc_mode = self.params.get('ipc_mode')
|
||||
current_ipc_mode = container_info['HostConfig'].get('IpcMode')
|
||||
if not current_ipc_mode:
|
||||
current_ipc_mode = None
|
||||
|
||||
# only check IPC mode if it is specified
|
||||
if new_ipc_mode is not None and new_ipc_mode != current_ipc_mode:
|
||||
return True
|
||||
return False
|
||||
|
||||
def compare_cap_add(self, container_info):
|
||||
new_cap_add = self.params.get('cap_add', list())
|
||||
current_cap_add = container_info['HostConfig'].get('CapAdd',
|
||||
list())
|
||||
if not current_cap_add:
|
||||
current_cap_add = list()
|
||||
if set(new_cap_add).symmetric_difference(set(current_cap_add)):
|
||||
return True
|
||||
|
||||
def compare_security_opt(self, container_info):
|
||||
ipc_mode = self.params.get('ipc_mode')
|
||||
pid_mode = self.params.get('pid_mode')
|
||||
privileged = self.params.get('privileged', False)
|
||||
# NOTE(jeffrey4l) security opt is disabled when using host ipc mode or
|
||||
# host pid mode or privileged. So no need to compare security opts
|
||||
if ipc_mode == 'host' or pid_mode == 'host' or privileged:
|
||||
return False
|
||||
new_sec_opt = self.params.get('security_opt', list())
|
||||
current_sec_opt = container_info['HostConfig'].get('SecurityOpt',
|
||||
list())
|
||||
if not current_sec_opt:
|
||||
current_sec_opt = list()
|
||||
if set(new_sec_opt).symmetric_difference(set(current_sec_opt)):
|
||||
return True
|
||||
|
||||
def compare_pid_mode(self, container_info):
|
||||
new_pid_mode = self.params.get('pid_mode')
|
||||
current_pid_mode = container_info['HostConfig'].get('PidMode')
|
||||
@ -186,27 +113,6 @@ class DockerWorker(object):
|
||||
if new_pid_mode != current_pid_mode:
|
||||
return True
|
||||
|
||||
def compare_cgroupns_mode(self, container_info):
|
||||
if not self._cgroupns_mode_supported:
|
||||
return False
|
||||
new_cgroupns_mode = self.params.get('cgroupns_mode')
|
||||
if new_cgroupns_mode is None:
|
||||
# means we don't care what it is
|
||||
return False
|
||||
current_cgroupns_mode = (container_info['HostConfig']
|
||||
.get('CgroupnsMode'))
|
||||
if current_cgroupns_mode == '':
|
||||
# means the container was created on Docker pre-20.10
|
||||
# it behaves like 'host'
|
||||
current_cgroupns_mode = 'host'
|
||||
return new_cgroupns_mode != current_cgroupns_mode
|
||||
|
||||
def compare_privileged(self, container_info):
|
||||
new_privileged = self.params.get('privileged')
|
||||
current_privileged = container_info['HostConfig']['Privileged']
|
||||
if new_privileged != current_privileged:
|
||||
return True
|
||||
|
||||
def compare_image(self, container_info=None):
|
||||
container_info = container_info or self.get_container_info()
|
||||
parse_repository_tag = docker.utils.parse_repository_tag
|
||||
@ -224,42 +130,6 @@ class DockerWorker(object):
|
||||
parse_repository_tag(self.params.get('image'))):
|
||||
return True
|
||||
|
||||
def compare_labels(self, container_info):
|
||||
new_labels = self.params.get('labels')
|
||||
current_labels = container_info['Config'].get('Labels', dict())
|
||||
image_labels = self.check_image().get('Labels', dict())
|
||||
for k, v in image_labels.items():
|
||||
if k in new_labels:
|
||||
if v != new_labels[k]:
|
||||
return True
|
||||
else:
|
||||
del current_labels[k]
|
||||
|
||||
if new_labels != current_labels:
|
||||
return True
|
||||
|
||||
def compare_tmpfs(self, container_info):
|
||||
new_tmpfs = self.generate_tmpfs()
|
||||
current_tmpfs = container_info['HostConfig'].get('Tmpfs')
|
||||
if not new_tmpfs:
|
||||
new_tmpfs = []
|
||||
if not current_tmpfs:
|
||||
current_tmpfs = []
|
||||
|
||||
if set(current_tmpfs).symmetric_difference(set(new_tmpfs)):
|
||||
return True
|
||||
|
||||
def compare_volumes_from(self, container_info):
|
||||
new_vols_from = self.params.get('volumes_from')
|
||||
current_vols_from = container_info['HostConfig'].get('VolumesFrom')
|
||||
if not new_vols_from:
|
||||
new_vols_from = list()
|
||||
if not current_vols_from:
|
||||
current_vols_from = list()
|
||||
|
||||
if set(current_vols_from).symmetric_difference(set(new_vols_from)):
|
||||
return True
|
||||
|
||||
def compare_volumes(self, container_info):
|
||||
volumes, binds = self.generate_volumes()
|
||||
current_vols = container_info['Config'].get('Volumes')
|
||||
@ -282,109 +152,6 @@ class DockerWorker(object):
|
||||
if set(new_binds).symmetric_difference(set(current_binds)):
|
||||
return True
|
||||
|
||||
def compare_environment(self, container_info):
|
||||
if self.params.get('environment'):
|
||||
current_env = dict()
|
||||
for kv in container_info['Config'].get('Env', list()):
|
||||
k, v = kv.split('=', 1)
|
||||
current_env.update({k: v})
|
||||
|
||||
for k, v in self.params.get('environment').items():
|
||||
if k not in current_env:
|
||||
return True
|
||||
if current_env[k] != v:
|
||||
return True
|
||||
|
||||
def compare_container_state(self, container_info):
|
||||
new_state = self.params.get('state')
|
||||
current_state = container_info['State'].get('Status')
|
||||
if new_state != current_state:
|
||||
return True
|
||||
|
||||
def compare_dimensions(self, container_info):
|
||||
new_dimensions = self.params.get('dimensions')
|
||||
# NOTE(mgoddard): The names used by Docker are inconsisent between
|
||||
# configuration of a container's resources and the resources in
|
||||
# container_info['HostConfig']. This provides a mapping between the
|
||||
# two.
|
||||
dimension_map = {
|
||||
'mem_limit': 'Memory', 'mem_reservation': 'MemoryReservation',
|
||||
'memswap_limit': 'MemorySwap', 'cpu_period': 'CpuPeriod',
|
||||
'cpu_quota': 'CpuQuota', 'cpu_shares': 'CpuShares',
|
||||
'cpuset_cpus': 'CpusetCpus', 'cpuset_mems': 'CpusetMems',
|
||||
'blkio_weight': 'BlkioWeight', 'ulimits': 'Ulimits'}
|
||||
|
||||
if not self._dimensions_kernel_memory_removed:
|
||||
dimension_map['kernel_memory'] = 'KernelMemory'
|
||||
|
||||
unsupported = set(new_dimensions.keys()) - \
|
||||
set(dimension_map.keys())
|
||||
if unsupported:
|
||||
self.module.exit_json(
|
||||
failed=True, msg=repr("Unsupported dimensions"),
|
||||
unsupported_dimensions=unsupported)
|
||||
current_dimensions = container_info['HostConfig']
|
||||
for key1, key2 in dimension_map.items():
|
||||
# NOTE(mgoddard): If a resource has been explicitly requested,
|
||||
# check for a match. Otherwise, ensure it is set to the default.
|
||||
if key1 in new_dimensions:
|
||||
if key1 == 'ulimits':
|
||||
if self.compare_ulimits(new_dimensions[key1],
|
||||
current_dimensions[key2]):
|
||||
return True
|
||||
elif new_dimensions[key1] != current_dimensions[key2]:
|
||||
return True
|
||||
elif current_dimensions[key2]:
|
||||
# The default values of all currently supported resources are
|
||||
# '' or 0 - both falsey.
|
||||
return True
|
||||
|
||||
def compare_ulimits(self, new_ulimits, current_ulimits):
|
||||
# The new_ulimits is dict, we need make it to a list of Ulimit
|
||||
# instance.
|
||||
new_ulimits = self.build_ulimits(new_ulimits)
|
||||
|
||||
def key(ulimit):
|
||||
return ulimit['Name']
|
||||
|
||||
if current_ulimits is None:
|
||||
current_ulimits = []
|
||||
return sorted(new_ulimits, key=key) != sorted(current_ulimits, key=key)
|
||||
|
||||
def compare_command(self, container_info):
|
||||
new_command = self.params.get('command')
|
||||
if new_command is not None:
|
||||
new_command_split = shlex.split(new_command)
|
||||
new_path = new_command_split[0]
|
||||
new_args = new_command_split[1:]
|
||||
if (new_path != container_info['Path'] or
|
||||
new_args != container_info['Args']):
|
||||
return True
|
||||
|
||||
def compare_healthcheck(self, container_info):
|
||||
new_healthcheck = self.parse_healthcheck(
|
||||
self.params.get('healthcheck'))
|
||||
current_healthcheck = container_info['Config'].get('Healthcheck')
|
||||
|
||||
healthcheck_map = {
|
||||
'test': 'Test',
|
||||
'retries': 'Retries',
|
||||
'interval': 'Interval',
|
||||
'start_period': 'StartPeriod',
|
||||
'timeout': 'Timeout'}
|
||||
|
||||
if new_healthcheck:
|
||||
new_healthcheck = new_healthcheck['healthcheck']
|
||||
if current_healthcheck:
|
||||
new_healthcheck = dict((healthcheck_map.get(k, k), v)
|
||||
for (k, v) in new_healthcheck.items())
|
||||
return new_healthcheck != current_healthcheck
|
||||
else:
|
||||
return True
|
||||
else:
|
||||
if current_healthcheck:
|
||||
return True
|
||||
|
||||
def compare_config(self):
|
||||
try:
|
||||
job = self.dc.exec_create(
|
||||
@ -422,19 +189,6 @@ class DockerWorker(object):
|
||||
'ExitCode: %s Message: %s' %
|
||||
(exec_inspect['ExitCode'], output))
|
||||
|
||||
def parse_image(self):
|
||||
full_image = self.params.get('image')
|
||||
|
||||
if '/' in full_image:
|
||||
registry, image = full_image.split('/', 1)
|
||||
else:
|
||||
image = full_image
|
||||
|
||||
if ':' in image:
|
||||
return full_image.rsplit(':', 1)
|
||||
else:
|
||||
return full_image, 'latest'
|
||||
|
||||
def get_image_id(self):
|
||||
full_image = self.params.get('image')
|
||||
|
||||
@ -490,49 +244,11 @@ class DockerWorker(object):
|
||||
container=self.params.get('name'),
|
||||
force=True
|
||||
)
|
||||
self.systemd.remove_unit_file()
|
||||
except docker.errors.APIError:
|
||||
if self.check_container():
|
||||
raise
|
||||
|
||||
def generate_tmpfs(self):
|
||||
tmpfs = self.params.get('tmpfs')
|
||||
if tmpfs:
|
||||
# NOTE(mgoddard): Filter out any empty strings.
|
||||
tmpfs = [t for t in tmpfs if t]
|
||||
return tmpfs
|
||||
|
||||
def generate_volumes(self):
|
||||
volumes = self.params.get('volumes')
|
||||
if not volumes:
|
||||
return None, None
|
||||
|
||||
vol_list = list()
|
||||
vol_dict = dict()
|
||||
|
||||
for vol in volumes:
|
||||
if len(vol) == 0:
|
||||
continue
|
||||
|
||||
if ':' not in vol:
|
||||
vol_list.append(vol)
|
||||
continue
|
||||
|
||||
split_vol = vol.split(':')
|
||||
|
||||
if (len(split_vol) == 2 and
|
||||
('/' not in split_vol[0] or '/' in split_vol[1])):
|
||||
split_vol.append('rw')
|
||||
|
||||
vol_list.append(split_vol[1])
|
||||
vol_dict.update({
|
||||
split_vol[0]: {
|
||||
'bind': split_vol[1],
|
||||
'mode': split_vol[2]
|
||||
}
|
||||
})
|
||||
|
||||
return vol_list, vol_dict
|
||||
|
||||
def parse_dimensions(self, dimensions):
|
||||
# When the data object contains types such as
|
||||
# docker.types.Ulimit, Ansible will fail when these are
|
||||
@ -717,95 +433,6 @@ class DockerWorker(object):
|
||||
**self.result
|
||||
)
|
||||
|
||||
def get_container_env(self):
|
||||
name = self.params.get('name')
|
||||
info = self.get_container_info()
|
||||
if not info:
|
||||
self.module.fail_json(msg="No such container: {}".format(name))
|
||||
else:
|
||||
envs = dict()
|
||||
for env in info['Config']['Env']:
|
||||
if '=' in env:
|
||||
key, value = env.split('=', 1)
|
||||
else:
|
||||
key, value = env, ''
|
||||
envs[key] = value
|
||||
|
||||
self.module.exit_json(**envs)
|
||||
|
||||
def get_container_state(self):
|
||||
name = self.params.get('name')
|
||||
info = self.get_container_info()
|
||||
if not info:
|
||||
self.module.fail_json(msg="No such container: {}".format(name))
|
||||
else:
|
||||
self.module.exit_json(**info['State'])
|
||||
|
||||
def parse_healthcheck(self, healthcheck):
|
||||
if not healthcheck:
|
||||
return None
|
||||
|
||||
result = dict(healthcheck={})
|
||||
|
||||
# All supported healthcheck parameters
|
||||
supported = set(['test', 'interval', 'timeout', 'start_period',
|
||||
'retries'])
|
||||
unsupported = set(healthcheck) - supported
|
||||
missing = supported - set(healthcheck)
|
||||
duration_options = set(['interval', 'timeout', 'start_period'])
|
||||
|
||||
if unsupported:
|
||||
self.module.exit_json(failed=True,
|
||||
msg=repr("Unsupported healthcheck options"),
|
||||
unsupported_healthcheck=unsupported)
|
||||
|
||||
if missing:
|
||||
self.module.exit_json(failed=True,
|
||||
msg=repr("Missing healthcheck option"),
|
||||
missing_healthcheck=missing)
|
||||
|
||||
for key in healthcheck:
|
||||
value = healthcheck.get(key)
|
||||
if key in duration_options:
|
||||
try:
|
||||
result['healthcheck'][key] = int(value) * 1000000000
|
||||
except TypeError:
|
||||
raise TypeError(
|
||||
'Cannot parse healthcheck "{0}". '
|
||||
'Expected an integer, got "{1}".'
|
||||
.format(value, type(value).__name__)
|
||||
)
|
||||
except ValueError:
|
||||
raise ValueError(
|
||||
'Cannot parse healthcheck "{0}". '
|
||||
'Expected an integer, got "{1}".'
|
||||
.format(value, type(value).__name__)
|
||||
)
|
||||
else:
|
||||
if key == 'test':
|
||||
# If the user explicitly disables the healthcheck,
|
||||
# return None as the healthcheck object
|
||||
if value in (['NONE'], 'NONE'):
|
||||
return None
|
||||
else:
|
||||
if isinstance(value, (tuple, list)):
|
||||
result['healthcheck'][key] = \
|
||||
[str(e) for e in value]
|
||||
else:
|
||||
result['healthcheck'][key] = \
|
||||
['CMD-SHELL', str(value)]
|
||||
elif key == 'retries':
|
||||
try:
|
||||
result['healthcheck'][key] = int(value)
|
||||
except ValueError:
|
||||
raise ValueError(
|
||||
'Cannot parse healthcheck number of retries.'
|
||||
'Expected an integer, got "{0}".'
|
||||
.format(type(value))
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
def stop_container(self):
|
||||
name = self.params.get('name')
|
||||
graceful_timeout = self.params.get('graceful_timeout')
|
||||
|
0
tests/kolla_docker_tests/__init__.py
Normal file
0
tests/kolla_docker_tests/__init__.py
Normal file
1713
tests/kolla_docker_tests/test_docker_worker.py
Normal file
1713
tests/kolla_docker_tests/test_docker_worker.py
Normal file
File diff suppressed because it is too large
Load Diff
239
tests/kolla_docker_tests/test_systemd.py
Normal file
239
tests/kolla_docker_tests/test_systemd.py
Normal file
@ -0,0 +1,239 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
# Copyright 2016 NEC Corporation
|
||||
# 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 imp
|
||||
import os
|
||||
import sys
|
||||
from unittest import mock
|
||||
|
||||
from oslotest import base
|
||||
|
||||
sys.modules['dbus'] = mock.MagicMock()
|
||||
|
||||
this_dir = os.path.dirname(sys.modules[__name__].__file__)
|
||||
ansible_dir = os.path.join(this_dir, '..', '..', 'ansible')
|
||||
systemd_worker_file = os.path.join(ansible_dir,
|
||||
'module_utils', 'kolla_systemd_worker.py')
|
||||
swm = imp.load_source('kolla_systemd_worker', systemd_worker_file)
|
||||
|
||||
|
||||
class TestSystemd(base.BaseTestCase):
|
||||
def setUp(self) -> None:
|
||||
super(TestSystemd, self).setUp()
|
||||
self.params_dict = dict(
|
||||
name='test',
|
||||
restart_policy='no',
|
||||
client_timeout=120,
|
||||
restart_retries=10,
|
||||
graceful_timeout=15
|
||||
)
|
||||
swm.sleep = mock.Mock()
|
||||
self.sw = swm.SystemdWorker(self.params_dict)
|
||||
|
||||
def test_manager(self):
|
||||
self.assertIsNotNone(self.sw)
|
||||
self.assertIsNotNone(self.sw.manager)
|
||||
|
||||
def test_start(self):
|
||||
self.sw.perform_action = mock.Mock(return_value=True)
|
||||
self.sw.wait_for_unit = mock.Mock(return_value=True)
|
||||
|
||||
self.sw.start()
|
||||
self.sw.perform_action.assert_called_once_with(
|
||||
'StartUnit',
|
||||
'kolla-test-container.service',
|
||||
'replace'
|
||||
)
|
||||
|
||||
def test_restart(self):
|
||||
self.sw.perform_action = mock.Mock(return_value=True)
|
||||
self.sw.wait_for_unit = mock.Mock(return_value=True)
|
||||
|
||||
self.sw.restart()
|
||||
self.sw.perform_action.assert_called_once_with(
|
||||
'RestartUnit',
|
||||
'kolla-test-container.service',
|
||||
'replace'
|
||||
)
|
||||
|
||||
def test_stop(self):
|
||||
self.sw.perform_action = mock.Mock(return_value=True)
|
||||
|
||||
self.sw.stop()
|
||||
self.sw.perform_action.assert_called_once_with(
|
||||
'StopUnit',
|
||||
'kolla-test-container.service',
|
||||
'replace'
|
||||
)
|
||||
|
||||
def test_reload(self):
|
||||
self.sw.perform_action = mock.Mock(return_value=True)
|
||||
|
||||
self.sw.reload()
|
||||
self.sw.perform_action.assert_called_once_with(
|
||||
'Reload',
|
||||
'kolla-test-container.service',
|
||||
'replace'
|
||||
)
|
||||
|
||||
def test_enable(self):
|
||||
self.sw.perform_action = mock.Mock(return_value=True)
|
||||
|
||||
self.sw.enable()
|
||||
self.sw.perform_action.assert_called_once_with(
|
||||
'EnableUnitFiles',
|
||||
['kolla-test-container.service'],
|
||||
False,
|
||||
True
|
||||
)
|
||||
|
||||
def test_check_unit_change(self):
|
||||
self.sw.generate_unit_file = mock.Mock()
|
||||
self.sw.check_unit_file = mock.Mock(return_value=True)
|
||||
open_mock = mock.mock_open(read_data='test data')
|
||||
return_val = None
|
||||
|
||||
with mock.patch('builtins.open', open_mock, create=True):
|
||||
return_val = self.sw.check_unit_change('test data')
|
||||
|
||||
self.assertFalse(return_val)
|
||||
self.sw.generate_unit_file.assert_not_called()
|
||||
open_mock.assert_called_with(
|
||||
'/etc/systemd/system/kolla-test-container.service',
|
||||
'r'
|
||||
)
|
||||
open_mock.return_value.read.assert_called_once()
|
||||
|
||||
def test_check_unit_change_diff(self):
|
||||
self.sw.generate_unit_file = mock.Mock()
|
||||
self.sw.check_unit_file = mock.Mock(return_value=True)
|
||||
open_mock = mock.mock_open(read_data='new data')
|
||||
return_val = None
|
||||
|
||||
with mock.patch('builtins.open', open_mock, create=True):
|
||||
return_val = self.sw.check_unit_change('old data')
|
||||
|
||||
self.assertTrue(return_val)
|
||||
self.sw.generate_unit_file.assert_not_called()
|
||||
open_mock.assert_called_with(
|
||||
'/etc/systemd/system/kolla-test-container.service',
|
||||
'r'
|
||||
)
|
||||
open_mock.return_value.read.assert_called_once()
|
||||
|
||||
@mock.patch(
|
||||
'kolla_systemd_worker.TEMPLATE',
|
||||
"""${name}, ${restart_policy},
|
||||
${graceful_timeout}, ${restart_timeout},
|
||||
${restart_retries}"""
|
||||
)
|
||||
def test_generate_unit_file(self):
|
||||
self.sw = swm.SystemdWorker(self.params_dict)
|
||||
p = self.params_dict
|
||||
ref_string = f"""{p.get('name')}, {p.get('restart_policy')},
|
||||
{p.get('graceful_timeout')}, {p.get('client_timeout')},
|
||||
{p.get('restart_retries')}"""
|
||||
|
||||
ret_string = self.sw.generate_unit_file()
|
||||
|
||||
self.assertEqual(ref_string, ret_string)
|
||||
|
||||
def test_create_unit_file(self):
|
||||
self.sw.generate_unit_file = mock.Mock(return_value='test data')
|
||||
self.sw.check_unit_change = mock.Mock(return_value=True)
|
||||
self.sw.reload = mock.Mock()
|
||||
self.sw.enable = mock.Mock()
|
||||
open_mock = mock.mock_open()
|
||||
return_val = None
|
||||
|
||||
with mock.patch('builtins.open', open_mock, create=True):
|
||||
return_val = self.sw.create_unit_file()
|
||||
|
||||
self.assertTrue(return_val)
|
||||
open_mock.assert_called_with(
|
||||
'/etc/systemd/system/kolla-test-container.service',
|
||||
'w'
|
||||
)
|
||||
open_mock.return_value.write.assert_called_once_with('test data')
|
||||
self.sw.reload.assert_called_once()
|
||||
self.sw.enable.assert_called_once()
|
||||
|
||||
def test_create_unit_file_no_change(self):
|
||||
self.sw.generate_unit_file = mock.Mock()
|
||||
self.sw.check_unit_change = mock.Mock(return_value=False)
|
||||
self.sw.reload = mock.Mock()
|
||||
self.sw.enable = mock.Mock()
|
||||
open_mock = mock.mock_open()
|
||||
|
||||
return_val = self.sw.create_unit_file()
|
||||
|
||||
self.assertFalse(return_val)
|
||||
open_mock.assert_not_called()
|
||||
self.sw.reload.assert_not_called()
|
||||
self.sw.enable.assert_not_called()
|
||||
|
||||
def test_remove_unit_file(self):
|
||||
self.sw.check_unit_file = mock.Mock(return_value=True)
|
||||
os.remove = mock.Mock()
|
||||
self.sw.reload = mock.Mock()
|
||||
|
||||
return_val = self.sw.remove_unit_file()
|
||||
|
||||
self.assertTrue(return_val)
|
||||
os.remove.assert_called_once_with(
|
||||
'/etc/systemd/system/kolla-test-container.service'
|
||||
)
|
||||
self.sw.reload.assert_called_once()
|
||||
|
||||
def test_get_unit_state(self):
|
||||
unit_list = [
|
||||
('foo.service', '', 'loaded', 'active', 'exited'),
|
||||
('kolla-test-container.service', '', 'loaded', 'active', 'running')
|
||||
]
|
||||
self.sw.manager.ListUnits = mock.Mock(return_value=unit_list)
|
||||
|
||||
state = self.sw.get_unit_state()
|
||||
|
||||
self.sw.manager.ListUnits.assert_called_once()
|
||||
self.assertEqual('running', state)
|
||||
|
||||
def test_get_unit_state_not_exist(self):
|
||||
unit_list = [
|
||||
('foo.service', '', 'loaded', 'active', 'exited'),
|
||||
('bar.service', '', 'loaded', 'active', 'running')
|
||||
]
|
||||
self.sw.manager.ListUnits = mock.Mock(return_value=unit_list)
|
||||
|
||||
state = self.sw.get_unit_state()
|
||||
|
||||
self.sw.manager.ListUnits.assert_called_once()
|
||||
self.assertIsNone(state)
|
||||
|
||||
def test_wait_for_unit(self):
|
||||
self.sw.get_unit_state = mock.Mock()
|
||||
self.sw.get_unit_state.side_effect = ['starting', 'running']
|
||||
|
||||
result = self.sw.wait_for_unit(10)
|
||||
|
||||
self.assertTrue(result)
|
||||
|
||||
def test_wait_for_unit_timeout(self):
|
||||
self.sw.get_unit_state = mock.Mock()
|
||||
self.sw.get_unit_state.side_effect = [
|
||||
'starting', 'starting', 'failed', 'failed']
|
||||
|
||||
result = self.sw.wait_for_unit(10)
|
||||
|
||||
self.assertFalse(result)
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user