
Docker run command has an option that automatically removes the container when it exits. This commit introduces the equivalent in Zun. In particular, this commit does the followings: * Introduce an 'auto_remove' field in container. If this field is set, the container will be automatically removed when it exits. * Introduce a new container state 'Delete'. This states indicated that the container has been automatically removed, but the record is still existed in DB. * Bump the docker REST API version to 1.25 or higher since 'auto_remove' feature was introduced in 1.25. There are several future work of this commit. The first task is to add a tempest test to verify the container is indeed auto-removed in docker daemon. The second task is to introduce a periodic task that purge container in 'Deleted' state. Co-Authored-By: Hongbin Lu <hongbin.lu@huawei.com> Co-Authored-By: Kien Nguyen <kiennt@vn.fujitsu.com> Related-Bug: #1644901 Change-Id: Ic6d35274a49648bde5e0e7486453a6d1a13f6f2e
282 lines
8.7 KiB
Python
282 lines
8.7 KiB
Python
# 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.
|
|
|
|
# It's based on oslo.i18n usage in OpenStack Keystone project and
|
|
# recommendations from
|
|
# https://docs.openstack.org/oslo.i18n/latest/user/usage.html
|
|
|
|
"""Utilities and helper functions."""
|
|
import eventlet
|
|
import functools
|
|
import mimetypes
|
|
import time
|
|
|
|
from oslo_concurrency import lockutils
|
|
from oslo_context import context as common_context
|
|
from oslo_log import log as logging
|
|
from oslo_service import loopingcall
|
|
import pecan
|
|
import six
|
|
|
|
from zun.common import consts
|
|
from zun.common import exception
|
|
from zun.common.i18n import _
|
|
import zun.conf
|
|
|
|
CONF = zun.conf.CONF
|
|
LOG = logging.getLogger(__name__)
|
|
|
|
synchronized = lockutils.synchronized_with_prefix('zun-')
|
|
|
|
VALID_STATES = {
|
|
'commit': [consts.RUNNING, consts.STOPPED, consts.PAUSED],
|
|
'delete': [consts.CREATED, consts.ERROR, consts.STOPPED, consts.DELETED],
|
|
'delete_force': [consts.CREATED, consts.CREATING, consts.ERROR,
|
|
consts.RUNNING, consts.STOPPED, consts.UNKNOWN,
|
|
consts.DELETED],
|
|
'start': [consts.CREATED, consts.STOPPED, consts.ERROR],
|
|
'stop': [consts.RUNNING],
|
|
'reboot': [consts.CREATED, consts.RUNNING, consts.STOPPED, consts.ERROR],
|
|
'pause': [consts.RUNNING],
|
|
'unpause': [consts.PAUSED],
|
|
'kill': [consts.RUNNING],
|
|
'execute': [consts.RUNNING],
|
|
'execute_resize': [consts.RUNNING],
|
|
'update': [consts.CREATED, consts.RUNNING, consts.STOPPED, consts.PAUSED],
|
|
'attach': [consts.RUNNING],
|
|
'resize': [consts.RUNNING],
|
|
'top': [consts.RUNNING],
|
|
'get_archive': [consts.CREATED, consts.PAUSED, consts.RUNNING,
|
|
consts.STOPPED],
|
|
'put_archive': [consts.CREATED, consts.PAUSED, consts.RUNNING,
|
|
consts.STOPPED],
|
|
'logs': [consts.CREATED, consts.ERROR, consts.PAUSED, consts.RUNNING,
|
|
consts.STOPPED, consts.UNKNOWN],
|
|
'stats': [consts.RUNNING],
|
|
'add_security_group': [consts.CREATED, consts.RUNNING, consts.STOPPED,
|
|
consts.PAUSED]
|
|
}
|
|
|
|
|
|
def validate_container_state(container, action):
|
|
if container.status not in VALID_STATES[action]:
|
|
raise exception.InvalidStateException(
|
|
id=container.uuid,
|
|
action=action,
|
|
actual_state=container.status)
|
|
|
|
|
|
def safe_rstrip(value, chars=None):
|
|
"""Removes trailing characters from a string if that does not make it empty
|
|
|
|
:param value: A string value that will be stripped.
|
|
:param chars: Characters to remove.
|
|
:return: Stripped value.
|
|
|
|
"""
|
|
if not isinstance(value, six.string_types):
|
|
LOG.warning((
|
|
"Failed to remove trailing character. Returning original object. "
|
|
"Supplied object is not a string: %s."
|
|
), value)
|
|
return value
|
|
|
|
return value.rstrip(chars) or value
|
|
|
|
|
|
def _do_allow_certain_content_types(func, content_types_list):
|
|
# Allows you to bypass pecan's content-type restrictions
|
|
cfg = pecan.util._cfg(func)
|
|
cfg.setdefault('content_types', {})
|
|
cfg['content_types'].update((value, '')
|
|
for value in content_types_list)
|
|
return func
|
|
|
|
|
|
def allow_certain_content_types(*content_types_list):
|
|
def _wrapper(func):
|
|
return _do_allow_certain_content_types(func, content_types_list)
|
|
return _wrapper
|
|
|
|
|
|
def allow_all_content_types(f):
|
|
return _do_allow_certain_content_types(f, mimetypes.types_map.values())
|
|
|
|
|
|
def parse_image_name(image):
|
|
image_parts = image.split(':', 1)
|
|
|
|
image_repo = image_parts[0]
|
|
image_tag = 'latest'
|
|
|
|
if len(image_parts) > 1:
|
|
image_tag = image_parts[1]
|
|
|
|
return image_repo, image_tag
|
|
|
|
|
|
def spawn_n(func, *args, **kwargs):
|
|
"""Passthrough method for eventlet.spawn_n.
|
|
|
|
This utility exists so that it can be stubbed for testing without
|
|
interfering with the service spawns.
|
|
|
|
It will also grab the context from the threadlocal store and add it to
|
|
the store on the new thread. This allows for continuity in logging the
|
|
context when using this method to spawn a new thread.
|
|
"""
|
|
_context = common_context.get_current()
|
|
|
|
@functools.wraps(func)
|
|
def context_wrapper(*args, **kwargs):
|
|
# NOTE: If update_store is not called after spawn_n it won't be
|
|
# available for the logger to pull from threadlocal storage.
|
|
if _context is not None:
|
|
_context.update_store()
|
|
func(*args, **kwargs)
|
|
|
|
eventlet.spawn_n(context_wrapper, *args, **kwargs)
|
|
|
|
|
|
def translate_exception(function):
|
|
"""Wraps a method to catch exceptions.
|
|
|
|
If the exception is not an instance of ZunException,
|
|
translate it into one.
|
|
"""
|
|
|
|
@functools.wraps(function)
|
|
def decorated_function(self, context, *args, **kwargs):
|
|
try:
|
|
return function(self, context, *args, **kwargs)
|
|
except Exception as e:
|
|
if not isinstance(e, exception.ZunException):
|
|
LOG.exception("Unexpected error: %s", six.text_type(e))
|
|
e = exception.ZunException("Unexpected error: %s"
|
|
% six.text_type(e))
|
|
raise e
|
|
raise
|
|
|
|
return decorated_function
|
|
|
|
|
|
def check_container_id(function):
|
|
"""Check container_id property of given container instance."""
|
|
|
|
@functools.wraps(function)
|
|
def decorated_function(*args, **kwargs):
|
|
container = args[2]
|
|
if getattr(container, 'container_id', None) is None:
|
|
msg = _("Cannot operate an uncreated container.")
|
|
raise exception.Invalid(message=msg)
|
|
return function(*args, **kwargs)
|
|
|
|
return decorated_function
|
|
|
|
|
|
def poll_until(retriever, condition=lambda value: value,
|
|
sleep_time=1, time_out=None, success_msg=None,
|
|
timeout_msg=None):
|
|
"""Retrieves object until it passes condition, then returns it.
|
|
|
|
If time_out_limit is passed in, PollTimeOut will be raised once that
|
|
amount of time is elapsed.
|
|
"""
|
|
start_time = time.time()
|
|
|
|
def poll_and_check():
|
|
obj = retriever()
|
|
if condition(obj):
|
|
raise loopingcall.LoopingCallDone(retvalue=obj)
|
|
if time_out is not None and time.time() - start_time > time_out:
|
|
raise exception.PollTimeOut
|
|
|
|
try:
|
|
poller = loopingcall.FixedIntervalLoopingCall(
|
|
f=poll_and_check).start(sleep_time, initial_delay=False)
|
|
poller.wait()
|
|
LOG.info(success_msg)
|
|
except exception.PollTimeOut:
|
|
LOG.error(timeout_msg)
|
|
raise
|
|
except Exception as e:
|
|
LOG.exception("Unexpected exception occurred: %s",
|
|
six.text_type(e))
|
|
raise
|
|
|
|
|
|
def get_image_pull_policy(image_pull_policy, image_tag):
|
|
if not image_pull_policy:
|
|
if image_tag == 'latest':
|
|
image_pull_policy = 'always'
|
|
else:
|
|
image_pull_policy = 'ifnotpresent'
|
|
return image_pull_policy
|
|
|
|
|
|
def should_pull_image(image_pull_policy, present):
|
|
if image_pull_policy == 'never':
|
|
return False
|
|
if (image_pull_policy == 'always' or
|
|
(image_pull_policy == 'ifnotpresent' and not present)):
|
|
return True
|
|
return False
|
|
|
|
|
|
def get_floating_cpu_set():
|
|
"""Parse floating_cpu_set config.
|
|
|
|
:returns: a set of pcpu ids can be used by containers
|
|
"""
|
|
|
|
if not CONF.floating_cpu_set:
|
|
return None
|
|
|
|
cpuset_ids = parse_floating_cpu(CONF.floating_cpu_set)
|
|
if not cpuset_ids:
|
|
raise exception.Invalid(_("No CPUs available after parsing %r") %
|
|
CONF.floating_cpu_set)
|
|
return cpuset_ids
|
|
|
|
|
|
def parse_floating_cpu(spec):
|
|
"""Parse a CPU set specification.
|
|
|
|
Each element in the list is either a single CPU number, a range of
|
|
CPU numbers.
|
|
|
|
:param spec: cpu set string eg "1-4,6"
|
|
:returns: a set of CPU indexes
|
|
|
|
"""
|
|
|
|
cpuset_ids = set()
|
|
for rule in spec.split(','):
|
|
range_part = rule.strip().split("-", 1)
|
|
if len(range_part) > 1:
|
|
try:
|
|
start, end = [int(p.strip()) for p in range_part]
|
|
except ValueError:
|
|
raise exception.Invalid()
|
|
if start < end:
|
|
cpuset_ids |= set(range(start, end + 1))
|
|
else:
|
|
raise exception.Invalid()
|
|
else:
|
|
try:
|
|
cpuset_ids.add(int(rule))
|
|
except ValueError:
|
|
raise exception.Invalid()
|
|
|
|
return cpuset_ids
|