Browse Source

Remove container object

Following on from removing the k8s specific APIs in
I1f6f04a35dfbb39f217487fea104ded035b75569 the objects associated with
these APIs need removal.

Remove the container object, drop the db table and remove references to
the container object. The docker_conductor has also been removed as this
was used for managing containers using Magnum objects.

Change-Id: I288fa7a9717519b1ae8195820975676d99b4d6d2
Partially-Implements: blueprint delete-container-endpoint
Co-Authored-By: Spyros Trigazis <strigazi@gmail.com>
changes/70/324470/2
Tom Cammann 5 years ago
committed by Spyros Trigazis
parent
commit
40aa6550f1
  1. 7
      etc/magnum/policy.json
  2. 2
      magnum/cmd/conductor.py
  3. 11
      magnum/common/docker_utils.py
  4. 33
      magnum/conductor/api.py
  5. 212
      magnum/conductor/handlers/docker_conductor.py
  6. 4
      magnum/conductor/monitors.py
  7. 84
      magnum/db/api.py
  8. 28
      magnum/db/sqlalchemy/alembic/versions/1f196a3dabae_remove_container.py
  9. 102
      magnum/db/sqlalchemy/api.py
  10. 21
      magnum/db/sqlalchemy/models.py
  11. 3
      magnum/objects/__init__.py
  12. 186
      magnum/objects/container.py
  13. 1
      magnum/opts.py
  14. 9
      magnum/tests/fake_policy.py
  15. 117
      magnum/tests/unit/common/test_docker_utils.py
  16. 558
      magnum/tests/unit/conductor/handlers/test_docker_conductor.py
  17. 62
      magnum/tests/unit/conductor/test_rpcapi.py
  18. 7
      magnum/tests/unit/conductor/test_utils.py
  19. 18
      magnum/tests/unit/db/test_bay.py
  20. 155
      magnum/tests/unit/db/test_container.py
  21. 33
      magnum/tests/unit/db/utils.py
  22. 141
      magnum/tests/unit/objects/test_container.py
  23. 1
      magnum/tests/unit/objects/test_objects.py
  24. 24
      magnum/tests/unit/objects/utils.py

7
etc/magnum/policy.json

@ -27,13 +27,6 @@
"rc:get_all": "rule:default",
"rc:update": "rule:default",
"container:create": "rule:admin_or_user",
"container:delete": "rule:admin_or_user",
"container:detail": "rule:default",
"container:get": "rule:default",
"container:get_all": "rule:default",
"container:update": "rule:admin_or_user",
"certificate:create": "rule:admin_or_user",
"certificate:get": "rule:admin_or_user",

2
magnum/cmd/conductor.py

@ -28,7 +28,6 @@ from magnum.common import short_id
from magnum.conductor.handlers import bay_conductor
from magnum.conductor.handlers import ca_conductor
from magnum.conductor.handlers import conductor_listener
from magnum.conductor.handlers import docker_conductor
from magnum.conductor.handlers import indirection_api
from magnum.conductor.handlers import k8s_conductor
from magnum.i18n import _LI
@ -51,7 +50,6 @@ def main():
conductor_id = short_id.generate_id()
endpoints = [
indirection_api.Handler(),
docker_conductor.Handler(),
k8s_conductor.Handler(),
bay_conductor.Handler(),
conductor_listener.Handler(),

11
magnum/common/docker_utils.py

@ -18,11 +18,9 @@ from docker import client
from docker import tls
from docker.utils import utils
from oslo_config import cfg
from oslo_utils import uuidutils
from magnum.conductor.handlers.common import cert_manager
from magnum.conductor import utils as conductor_utils
from magnum import objects
docker_opts = [
@ -76,15 +74,6 @@ def is_docker_api_version_atleast(docker, version):
return False
@contextlib.contextmanager
def docker_for_container(context, container):
if uuidutils.is_uuid_like(container):
container = objects.Container.get_by_uuid(context, container)
bay = conductor_utils.retrieve_bay(context, container.bay_uuid)
with docker_for_bay(context, bay) as docker:
yield docker
@contextlib.contextmanager
def docker_for_bay(context, bay):
baymodel = conductor_utils.retrieve_baymodel(context, bay)

33
magnum/conductor/api.py

@ -59,39 +59,6 @@ class API(rpc_service.API):
return self._call('rc_show', rc_ident=rc_ident,
bay_ident=bay_ident)
# Container operations
def container_create(self, container):
return self._call('container_create', container=container)
def container_delete(self, container_uuid):
return self._call('container_delete', container_uuid=container_uuid)
def container_show(self, container_uuid):
return self._call('container_show', container_uuid=container_uuid)
def container_reboot(self, container_uuid):
return self._call('container_reboot', container_uuid=container_uuid)
def container_stop(self, container_uuid):
return self._call('container_stop', container_uuid=container_uuid)
def container_start(self, container_uuid):
return self._call('container_start', container_uuid=container_uuid)
def container_pause(self, container_uuid):
return self._call('container_pause', container_uuid=container_uuid)
def container_unpause(self, container_uuid):
return self._call('container_unpause', container_uuid=container_uuid)
def container_logs(self, container_uuid):
return self._call('container_logs', container_uuid=container_uuid)
def container_exec(self, container_uuid, command):
return self._call('container_exec', container_uuid=container_uuid,
command=command)
# CA operations
def sign_certificate(self, bay, certificate):

212
magnum/conductor/handlers/docker_conductor.py

@ -1,212 +0,0 @@
# 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.
"""Magnum Docker RPC handler."""
import functools
from docker import errors
from oslo_log import log as logging
import six
from magnum.common import docker_utils
from magnum.common import exception
from magnum.common import utils as magnum_utils
from magnum.i18n import _LE
from magnum import objects
from magnum.objects import fields
LOG = logging.getLogger(__name__)
def wrap_container_exception(f):
def wrapped(self, context, *args, **kwargs):
try:
return f(self, context, *args, **kwargs)
except Exception as e:
container_uuid = None
if 'container_uuid' in kwargs:
container_uuid = kwargs.get('container_uuid')
elif 'container' in kwargs:
container_uuid = kwargs.get('container').uuid
LOG.exception(_LE("Error while connect to docker "
"container %s"), container_uuid)
raise exception.ContainerException(
"Docker internal Error: %s" % str(e))
return functools.wraps(f)(wrapped)
class Handler(object):
def __init__(self):
super(Handler, self).__init__()
@staticmethod
def _find_container_by_name(docker, name):
try:
for info in docker.list_instances(inspect=True):
if info['Config'].get('Hostname') == name:
return info
except errors.APIError as e:
if e.response.status_code != 404:
raise
return {}
def _encode_utf8(self, value):
if six.PY2 and not isinstance(value, unicode):
value = unicode(value)
return value.encode('utf-8')
# Container operations
@wrap_container_exception
def container_create(self, context, container):
with docker_utils.docker_for_container(context, container) as docker:
name = container.name
container_uuid = container.uuid
image = container.image
LOG.debug('Creating container with image %s name %s', image, name)
try:
image_repo, image_tag = docker_utils.parse_docker_image(image)
docker.pull(image_repo, tag=image_tag)
docker.inspect_image(self._encode_utf8(container.image))
kwargs = {'name': name,
'hostname': container_uuid,
'command': container.command,
'environment': container.environment}
if docker_utils.is_docker_api_version_atleast(docker, '1.19'):
if container.memory is not None:
kwargs['host_config'] = {
'Memory':
magnum_utils.get_docker_quantity(container.memory)}
else:
kwargs['mem_limit'] = container.memory
docker.create_container(image, **kwargs)
container.status = fields.ContainerStatus.STOPPED
return container
except errors.APIError:
container.status = fields.ContainerStatus.ERROR
raise
finally:
container.save()
@wrap_container_exception
def container_delete(self, context, container_uuid):
LOG.debug("container_delete %s", container_uuid)
with docker_utils.docker_for_container(context,
container_uuid) as docker:
docker_id = self._find_container_by_name(docker,
container_uuid)
if not docker_id:
return None
return docker.remove_container(docker_id)
@wrap_container_exception
def container_show(self, context, container_uuid):
LOG.debug("container_show %s", container_uuid)
with docker_utils.docker_for_container(context,
container_uuid) as docker:
container = objects.Container.get_by_uuid(context, container_uuid)
try:
docker_id = self._find_container_by_name(docker,
container_uuid)
if not docker_id:
LOG.exception(_LE("Can not find docker instance with %s,"
"set it to Error status"),
container_uuid)
container.status = fields.ContainerStatus.ERROR
container.save()
return container
result = docker.inspect_container(docker_id)
status = result.get('State')
if status:
if status.get('Error') is True:
container.status = fields.ContainerStatus.ERROR
elif status.get('Paused'):
container.status = fields.ContainerStatus.PAUSED
elif status.get('Running'):
container.status = fields.ContainerStatus.RUNNING
else:
container.status = fields.ContainerStatus.STOPPED
container.save()
return container
except errors.APIError as api_error:
error_message = str(api_error)
if '404' in error_message:
container.status = fields.ContainerStatus.ERROR
container.save()
return container
raise
@wrap_container_exception
def _container_action(self, context, container_uuid, status, docker_func):
LOG.debug("%s container %s ...", docker_func, container_uuid)
with docker_utils.docker_for_container(context,
container_uuid) as docker:
docker_id = self._find_container_by_name(docker,
container_uuid)
result = getattr(docker, docker_func)(docker_id)
container = objects.Container.get_by_uuid(context,
container_uuid)
container.status = status
container.save()
return result
def container_reboot(self, context, container_uuid):
return self._container_action(context, container_uuid,
fields.ContainerStatus.RUNNING,
'restart')
def container_stop(self, context, container_uuid):
return self._container_action(context, container_uuid,
fields.ContainerStatus.STOPPED, 'stop')
def container_start(self, context, container_uuid):
return self._container_action(context, container_uuid,
fields.ContainerStatus.RUNNING, 'start')
def container_pause(self, context, container_uuid):
return self._container_action(context, container_uuid,
fields.ContainerStatus.PAUSED, 'pause')
def container_unpause(self, context, container_uuid):
return self._container_action(context, container_uuid,
fields.ContainerStatus.RUNNING,
'unpause')
@wrap_container_exception
def container_logs(self, context, container_uuid):
LOG.debug("container_logs %s", container_uuid)
with docker_utils.docker_for_container(context,
container_uuid) as docker:
docker_id = self._find_container_by_name(docker,
container_uuid)
return {'output': docker.logs(docker_id)}
@wrap_container_exception
def container_exec(self, context, container_uuid, command):
LOG.debug("container_exec %s command %s",
container_uuid, command)
with docker_utils.docker_for_container(context,
container_uuid) as docker:
docker_id = self._find_container_by_name(docker,
container_uuid)
if docker_utils.is_docker_library_version_atleast('1.2.0'):
create_res = docker.exec_create(docker_id, command, True,
True, False)
exec_output = docker.exec_start(create_res, False, False,
False)
else:
exec_output = docker.execute(docker_id, command)
return {'output': exec_output}

4
magnum/conductor/monitors.py

@ -27,10 +27,10 @@ LOG = log.getLogger(__name__)
CONF = cfg.CONF
CONF.import_opt('docker_remote_api_version',
'magnum.conductor.handlers.docker_conductor',
'magnum.common.docker_utils',
group='docker')
CONF.import_opt('default_timeout',
'magnum.conductor.handlers.docker_conductor',
'magnum.common.docker_utils',
group='docker')
COE_CLASS_PATH = {

84
magnum/db/api.py

@ -204,90 +204,6 @@ class Connection(object):
:raises: BayModelNotFound
"""
@abc.abstractmethod
def get_container_list(self, context, filters=None,
limit=None, marker=None, sort_key=None,
sort_dir=None):
"""Get matching containers.
Return a list of the specified columns for all containers that match
the specified filters.
:param context: The security context
:param filters: Filters to apply. Defaults to None.
:param limit: Maximum number of containers to return.
:param marker: the last item of the previous page; we return the next
result set.
:param sort_key: Attribute by which results should be sorted.
:param sort_dir: direction in which results should be sorted.
(asc, desc)
:returns: A list of tuples of the specified columns.
"""
@abc.abstractmethod
def create_container(self, values):
"""Create a new container.
:param values: A dict containing several items used to identify
and track the container, and several dicts which are
passed
into the Drivers when managing this container. For
example:
::
{
'uuid': uuidutils.generate_uuid(),
'name': 'example',
'type': 'virt'
}
:returns: A container.
"""
@abc.abstractmethod
def get_container_by_id(self, context, container_id):
"""Return a container.
:param context: The security context
:param container_id: The id of a container.
:returns: A container.
"""
@abc.abstractmethod
def get_container_by_uuid(self, context, container_uuid):
"""Return a container.
:param context: The security context
:param container_uuid: The uuid of a container.
:returns: A container.
"""
@abc.abstractmethod
def get_container_by_name(self, context, container_name):
"""Return a container.
:param context: The security context
:param container_name: The name of a container.
:returns: A container.
"""
@abc.abstractmethod
def destroy_container(self, container_id):
"""Destroy a container and all associated interfaces.
:param container_id: The id or uuid of a container.
"""
@abc.abstractmethod
def update_container(self, container_id, values):
"""Update properties of a container.
:param container_id: The id or uuid of a container.
:returns: A container.
:raises: ContainerNotFound
"""
@abc.abstractmethod
def get_rc_list(self, context, filters=None, limit=None,
marker=None, sort_key=None, sort_dir=None):

28
magnum/db/sqlalchemy/alembic/versions/1f196a3dabae_remove_container.py

@ -0,0 +1,28 @@
# 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.
"""remove container object
Revision ID: 1f196a3dabae
Revises: e0653b2d5271
Create Date: 2016-06-02 11:42:42.200992
"""
# revision identifiers, used by Alembic.
revision = '1f196a3dabae'
down_revision = 'e0653b2d5271'
from alembic import op
def upgrade():
op.drop_table('container')

102
magnum/db/sqlalchemy/api.py

@ -198,11 +198,6 @@ class Connection(api.Connection):
if query.count() != 0:
query.delete()
query = model_query(models.Container, session=session)
query = self._add_containers_filters(query, {'bay_uuid': bay_uuid})
if query.count() != 0:
query.delete()
session = get_session()
with session.begin():
query = model_query(models.Bay, session=session)
@ -370,103 +365,6 @@ class Connection(api.Connection):
ref.update(values)
return ref
def _add_containers_filters(self, query, filters):
if filters is None:
filters = {}
filter_names = ['name', 'image', 'project_id', 'user_id',
'memory', 'bay_uuid']
for name in filter_names:
if name in filters:
query = query.filter_by(**{name: filters[name]})
return query
def get_container_list(self, context, filters=None, limit=None,
marker=None, sort_key=None, sort_dir=None):
query = model_query(models.Container)
query = self._add_tenant_filters(context, query)
query = self._add_containers_filters(query, filters)
return _paginate_query(models.Container, limit, marker,
sort_key, sort_dir, query)
def create_container(self, values):
# ensure defaults are present for new containers
if not values.get('uuid'):
values['uuid'] = uuidutils.generate_uuid()
container = models.Container()
container.update(values)
try:
container.save()
except db_exc.DBDuplicateEntry:
raise exception.ContainerAlreadyExists(uuid=values['uuid'])
return container
def get_container_by_id(self, context, container_id):
query = model_query(models.Container)
query = self._add_tenant_filters(context, query)
query = query.filter_by(id=container_id)
try:
return query.one()
except NoResultFound:
raise exception.ContainerNotFound(container=container_id)
def get_container_by_uuid(self, context, container_uuid):
query = model_query(models.Container)
query = self._add_tenant_filters(context, query)
query = query.filter_by(uuid=container_uuid)
try:
return query.one()
except NoResultFound:
raise exception.ContainerNotFound(container=container_uuid)
def get_container_by_name(self, context, container_name):
query = model_query(models.Container)
query = self._add_tenant_filters(context, query)
query = query.filter_by(name=container_name)
try:
return query.one()
except NoResultFound:
raise exception.ContainerNotFound(container=container_name)
except MultipleResultsFound:
raise exception.Conflict('Multiple containers exist with same '
'name. Please use the container uuid '
'instead.')
def destroy_container(self, container_id):
session = get_session()
with session.begin():
query = model_query(models.Container, session=session)
query = add_identity_filter(query, container_id)
count = query.delete()
if count != 1:
raise exception.ContainerNotFound(container_id)
def update_container(self, container_id, values):
# NOTE(dtantsur): this can lead to very strange errors
if 'uuid' in values:
msg = _("Cannot overwrite UUID for an existing Container.")
raise exception.InvalidParameterValue(err=msg)
return self._do_update_container(container_id, values)
def _do_update_container(self, container_id, values):
session = get_session()
with session.begin():
query = model_query(models.Container, session=session)
query = add_identity_filter(query, container_id)
try:
ref = query.with_lockmode('update').one()
except NoResultFound:
raise exception.ContainerNotFound(container=container_id)
if 'provision_state' in values:
values['provision_updated_at'] = timeutils.utcnow()
ref.update(values)
return ref
def _add_rcs_filters(self, query, filters):
if filters is None:
filters = {}

21
magnum/db/sqlalchemy/models.py

@ -179,27 +179,6 @@ class BayModel(Base):
master_lb_enabled = Column(Boolean, default=False)
class Container(Base):
"""Represents a container."""
__tablename__ = 'container'
__table_args__ = (
schema.UniqueConstraint('uuid', name='uniq_container0uuid'),
table_args()
)
id = Column(Integer, primary_key=True)
project_id = Column(String(255))
user_id = Column(String(255))
uuid = Column(String(36))
name = Column(String(255))
image = Column(String(255))
command = Column(String(255))
bay_uuid = Column(String(36))
status = Column(String(20))
memory = Column(String(255))
environment = Column(JSONEncodedDict)
class ReplicationController(Base):
"""Represents a pod replication controller."""

3
magnum/objects/__init__.py

@ -15,13 +15,11 @@
from magnum.objects import bay
from magnum.objects import baymodel
from magnum.objects import certificate
from magnum.objects import container
from magnum.objects import magnum_service
from magnum.objects import replicationcontroller as rc
from magnum.objects import x509keypair
Container = container.Container
Bay = bay.Bay
BayModel = baymodel.BayModel
MagnumService = magnum_service.MagnumService
@ -30,7 +28,6 @@ X509KeyPair = x509keypair.X509KeyPair
Certificate = certificate.Certificate
__all__ = (Bay,
BayModel,
Container,
MagnumService,
ReplicationController,
X509KeyPair,

186
magnum/objects/container.py

@ -1,186 +0,0 @@
# 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 oslo_versionedobjects import fields
from magnum.db import api as dbapi
from magnum.objects import base
from magnum.objects import fields as m_fields
@base.MagnumObjectRegistry.register
class Container(base.MagnumPersistentObject, base.MagnumObject,
base.MagnumObjectDictCompat):
# Version 1.0: Initial version
# Version 1.1: Add memory field
# Version 1.2: Add environment field
# Version 1.3: Add filters to list()
VERSION = '1.3'
dbapi = dbapi.get_instance()
fields = {
'id': fields.IntegerField(),
'uuid': fields.StringField(nullable=True),
'name': fields.StringField(nullable=True),
'project_id': fields.StringField(nullable=True),
'user_id': fields.StringField(nullable=True),
'image': fields.StringField(nullable=True),
'command': fields.StringField(nullable=True),
'bay_uuid': fields.StringField(nullable=True),
'status': m_fields.ContainerStatusField(nullable=True),
'memory': fields.StringField(nullable=True),
'environment': fields.DictOfStringsField(nullable=True),
}
@staticmethod
def _from_db_object(container, db_container):
"""Converts a database entity to a formal object."""
for field in container.fields:
container[field] = db_container[field]
container.obj_reset_changes()
return container
@staticmethod
def _from_db_object_list(db_objects, cls, context):
"""Converts a list of database entities to a list of formal objects."""
return [Container._from_db_object(cls(context), obj)
for obj in db_objects]
@base.remotable_classmethod
def get_by_id(cls, context, container_id):
"""Find a container based on its integer id and return a Container object.
:param container_id: the id of a container.
:param context: Security context
:returns: a :class:`Container` object.
"""
db_container = cls.dbapi.get_container_by_id(context, container_id)
container = Container._from_db_object(cls(context), db_container)
return container
@base.remotable_classmethod
def get_by_uuid(cls, context, uuid):
"""Find a container based on uuid and return a :class:`Container` object.
:param uuid: the uuid of a container.
:param context: Security context
:returns: a :class:`Container` object.
"""
db_container = cls.dbapi.get_container_by_uuid(context, uuid)
container = Container._from_db_object(cls(context), db_container)
return container
@base.remotable_classmethod
def get_by_name(cls, context, name):
"""Find a container based on name and return a Container object.
:param name: the logical name of a container.
:param context: Security context
:returns: a :class:`Container` object.
"""
db_bay = cls.dbapi.get_container_by_name(context, name)
bay = Container._from_db_object(cls(context), db_bay)
return bay
@base.remotable_classmethod
def list(cls, context, limit=None, marker=None,
sort_key=None, sort_dir=None, filters=None):
"""Return a list of Container objects.
:param context: Security context.
:param limit: maximum number of resources to return in a single result.
:param marker: pagination marker for large data sets.
:param sort_key: column to sort results by.
:param sort_dir: direction to sort. "asc" or "desc".
:param filters: filters when list containers, the filter name could be
'name', 'image', 'project_id', 'user_id', 'memory',
'bay_uuid'. For example, filters={'bay_uuid': '1'}
:returns: a list of :class:`Container` object.
"""
db_containers = cls.dbapi.get_container_list(context, limit=limit,
marker=marker,
sort_key=sort_key,
sort_dir=sort_dir,
filters=filters)
return Container._from_db_object_list(db_containers, cls, context)
@base.remotable
def create(self, context=None):
"""Create a Container record in the DB.
:param context: Security context. NOTE: This should only
be used internally by the indirection_api.
Unfortunately, RPC requires context as the first
argument, even though we don't use it.
A context should be set when instantiating the
object, e.g.: Container(context)
"""
values = self.obj_get_changes()
db_container = self.dbapi.create_container(values)
self._from_db_object(self, db_container)
@base.remotable
def destroy(self, context=None):
"""Delete the Container from the DB.
:param context: Security context. NOTE: This should only
be used internally by the indirection_api.
Unfortunately, RPC requires context as the first
argument, even though we don't use it.
A context should be set when instantiating the
object, e.g.: Container(context)
"""
self.dbapi.destroy_container(self.uuid)
self.obj_reset_changes()
@base.remotable
def save(self, context=None):
"""Save updates to this Container.
Updates will be made column by column based on the result
of self.what_changed().
:param context: Security context. NOTE: This should only
be used internally by the indirection_api.
Unfortunately, RPC requires context as the first
argument, even though we don't use it.
A context should be set when instantiating the
object, e.g.: Container(context)
"""
updates = self.obj_get_changes()
self.dbapi.update_container(self.uuid, updates)
self.obj_reset_changes()
@base.remotable
def refresh(self, context=None):
"""Loads updates for this Container.
Loads a container with the same uuid from the database and
checks for updated attributes. Updates are applied from
the loaded container column by column, if there are any updates.
:param context: Security context. NOTE: This should only
be used internally by the indirection_api.
Unfortunately, RPC requires context as the first
argument, even though we don't use it.
A context should be set when instantiating the
object, e.g.: Container(context)
"""
current = self.__class__.get_by_uuid(self._context, uuid=self.uuid)
for field in self.fields:
if self.obj_attr_is_set(field) and self[field] != current[field]:
self[field] = current[field]

1
magnum/opts.py

@ -25,7 +25,6 @@ import magnum.common.service
import magnum.common.x509.config
import magnum.conductor.config
import magnum.conductor.handlers.bay_conductor
import magnum.conductor.handlers.docker_conductor
import magnum.conductor.handlers.k8s_conductor
import magnum.db
import magnum.drivers.common.template_def

9
magnum/tests/fake_policy.py

@ -39,13 +39,6 @@ policy_data = """
"rc:detail": "",
"rc:get": "",
"rc:get_all": "",
"rc:update": "",
"container:create": "",
"container:delete": "",
"container:detail": "",
"container:get": "",
"container:get_all": "",
"container:update": ""
"rc:update": ""
}
"""

117
magnum/tests/unit/common/test_docker_utils.py

@ -29,123 +29,6 @@ CONF.import_opt('default_timeout', 'magnum.common.docker_utils',
class TestDockerUtils(base.BaseTestCase):
@mock.patch('magnum.common.docker_utils.DockerHTTPClient')
@mock.patch.object(docker_utils, 'cert_manager')
@mock.patch.object(docker_utils.objects.BayModel, 'get_by_uuid')
@mock.patch.object(docker_utils.objects.Bay, 'get_by_name')
def test_docker_for_container(self, mock_get_bay_by_name,
mock_get_baymodel_by_uuid,
mock_cert_manager,
mock_docker_client):
mock_container = mock.MagicMock()
mock_bay = mock.MagicMock()
mock_bay.api_address = 'https://1.2.3.4:2376'
mock_get_bay_by_name.return_value = mock_bay
mock_baymodel = mock.MagicMock()
mock_baymodel.tls_disabled = False
mock_get_baymodel_by_uuid.return_value = mock_baymodel
mock_ca_cert = mock.MagicMock()
mock_magnum_key = mock.MagicMock()
mock_magnum_cert = mock.MagicMock()
mock_cert_manager.create_client_files.return_value = (
mock_ca_cert, mock_magnum_key, mock_magnum_cert
)
mock_docker = mock.MagicMock()
mock_docker_client.return_value = mock_docker
with docker_utils.docker_for_container(mock.sentinel.context,
mock_container) as docker:
self.assertEqual(mock_docker, docker)
mock_get_bay_by_name.assert_called_once_with(mock.sentinel.context,
mock_container.bay_uuid)
mock_get_baymodel_by_uuid.assert_called_once_with(
mock.sentinel.context, mock_bay.baymodel_id)
mock_docker_client.assert_called_once_with(
'https://1.2.3.4:2376',
CONF.docker.docker_remote_api_version,
CONF.docker.default_timeout,
ca_cert=mock_ca_cert.name,
client_key=mock_magnum_key.name,
client_cert=mock_magnum_cert.name)
@mock.patch('magnum.common.docker_utils.DockerHTTPClient')
@mock.patch.object(docker_utils, 'cert_manager')
@mock.patch.object(docker_utils.objects.BayModel, 'get_by_uuid')
@mock.patch.object(docker_utils.objects.Bay, 'get_by_name')
@mock.patch.object(docker_utils.objects.Container, 'get_by_uuid')
def test_docker_for_container_uuid(self, mock_get_container_by_uuid,
mock_get_bay_by_name,
mock_get_baymodel_by_uuid,
mock_cert_manager,
mock_docker_client):
mock_container = mock.MagicMock()
mock_container.uuid = '8e48ffb1-754d-4f21-bdd0-1a39bf796389'
mock_get_container_by_uuid.return_value = mock_container
mock_bay = mock.MagicMock()
mock_bay.api_address = 'https://1.2.3.4:2376'
mock_get_bay_by_name.return_value = mock_bay
mock_baymodel = mock.MagicMock()
mock_baymodel.tls_disabled = False
mock_get_baymodel_by_uuid.return_value = mock_baymodel
mock_ca_cert = mock.MagicMock()
mock_magnum_key = mock.MagicMock()
mock_magnum_cert = mock.MagicMock()
mock_cert_manager.create_client_files.return_value = (
mock_ca_cert, mock_magnum_key, mock_magnum_cert
)
mock_docker = mock.MagicMock()
mock_docker_client.return_value = mock_docker
with docker_utils.docker_for_container(
mock.sentinel.context, mock_container.uuid) as docker:
self.assertEqual(mock_docker, docker)
mock_get_container_by_uuid.assert_called_once_with(
mock.sentinel.context, mock_container.uuid
)
mock_get_bay_by_name.assert_called_once_with(mock.sentinel.context,
mock_container.bay_uuid)
mock_get_baymodel_by_uuid.assert_called_once_with(
mock.sentinel.context, mock_bay.baymodel_id)
mock_docker_client.assert_called_once_with(
'https://1.2.3.4:2376',
CONF.docker.docker_remote_api_version,
CONF.docker.default_timeout,
ca_cert=mock_ca_cert.name,
client_key=mock_magnum_key.name,
client_cert=mock_magnum_cert.name)
@mock.patch('magnum.common.docker_utils.DockerHTTPClient')
@mock.patch.object(docker_utils.objects.BayModel, 'get_by_uuid')
@mock.patch.object(docker_utils.objects.Bay, 'get_by_name')
def test_docker_for_container_tls_disabled(self, mock_get_bay_by_name,
mock_get_baymodel_by_uuid,
mock_docker_client):
mock_container = mock.MagicMock()
mock_bay = mock.MagicMock()
mock_bay.api_address = 'tcp://1.2.3.4:2376'
mock_get_bay_by_name.return_value = mock_bay
mock_baymodel = mock.MagicMock()
mock_baymodel.tls_disabled = True
mock_get_baymodel_by_uuid.return_value = mock_baymodel
mock_docker = mock.MagicMock()
mock_docker_client.return_value = mock_docker
with docker_utils.docker_for_container(mock.sentinel.context,
mock_container) as docker:
self.assertEqual(mock_docker, docker)
mock_get_bay_by_name.assert_called_once_with(mock.sentinel.context,
mock_container.bay_uuid)
mock_get_baymodel_by_uuid.assert_called_once_with(
mock.sentinel.context, mock_bay.baymodel_id)
mock_docker_client.assert_called_once_with(
'tcp://1.2.3.4:2376',
CONF.docker.docker_remote_api_version,
CONF.docker.default_timeout)
def test_is_docker_api_version_atleast(self):

558
magnum/tests/unit/conductor/handlers/test_docker_conductor.py

@ -1,558 +0,0 @@
# Copyright 2015 Rackspace 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.
import docker
from docker import errors
import mock
import six
from magnum.common import docker_utils
from magnum.common import exception
from magnum.conductor.handlers import docker_conductor
from magnum import objects
from magnum.objects import fields
from magnum.tests import base
class TestDockerHandler(base.BaseTestCase):
def setUp(self):
super(TestDockerHandler, self).setUp()
self.conductor = docker_conductor.Handler()
dfc_patcher = mock.patch.object(docker_utils,
'docker_for_container')
docker_for_container = dfc_patcher.start()
self.dfc_context_manager = docker_for_container.return_value
self.mock_docker = mock.MagicMock()
self.dfc_context_manager.__enter__.return_value = self.mock_docker
self.addCleanup(dfc_patcher.stop)
@mock.patch.object(docker_utils, 'is_docker_api_version_atleast')
def _test_container_create(self, container_dict, expected_kwargs,
mock_version, expected_image='test_image',
expected_tag='some_tag',
api_version='1.18'):
mock_version.return_value = (float(api_version) > 1.18)
name = container_dict.pop('name')
mock_container = mock.MagicMock(**container_dict)
type(mock_container).name = mock.PropertyMock(return_value=name)
container = self.conductor.container_create(
None, mock_container)
utf8_image = self.conductor._encode_utf8(mock_container.image)
self.mock_docker.inspect_image.assert_called_once_with(utf8_image)
self.mock_docker.pull.assert_called_once_with(expected_image,
tag=expected_tag)
self.mock_docker.create_container.assert_called_once_with(
mock_container.image, **expected_kwargs)
self.assertEqual(fields.ContainerStatus.STOPPED, container.status)
def test_container_create(self):
container_dict = {
'name': 'some-name',
'uuid': 'some-uuid',
'image': 'test_image:some_tag',
'command': None,
'memory': None,
'environment': None,
}
expected_kwargs = {
'name': 'some-name',
'hostname': 'some-uuid',
'command': None,
'mem_limit': None,
'environment': None,
}
self._test_container_create(container_dict, expected_kwargs)
def test_container_create_api_1_19(self):
container_dict = {
'name': 'some-name',
'uuid': 'some-uuid',
'image': 'test_image:some_tag',
'command': None,
'memory': '100m',
'environment': None,
}
expected_kwargs = {
'name': 'some-name',
'hostname': 'some-uuid',
'command': None,
'host_config': {'Memory': 100 * 1024 * 1024},
'environment': None,
}
self._test_container_create(container_dict, expected_kwargs,
api_version='1.19')
def test_container_create_with_command(self):
container_dict = {
'name': 'some-name',
'uuid': 'some-uuid',
'image': 'test_image:some_tag',
'command': 'env',
'memory': None,
'environment': None,
}
expected_kwargs = {
'name': 'some-name',
'hostname': 'some-uuid',
'command': 'env',
'mem_limit': None,
'environment': None,
}
self._test_container_create(container_dict, expected_kwargs)
def test_container_create_with_memory(self):
container_dict = {
'name': 'some-name',
'uuid': 'some-uuid',
'image': 'test_image:some_tag',
'command': None,
'memory': '512m',
'environment': None,
}
expected_kwargs = {
'name': 'some-name',
'hostname': 'some-uuid',
'command': None,
'mem_limit': '512m',
'environment': None,
}
self._test_container_create(container_dict, expected_kwargs)
def test_container_create_with_environment(self):
container_dict = {
'name': 'some-name',
'uuid': 'some-uuid',
'image': 'test_image:some_tag',
'command': None,
'memory': '512m',
'environment': {'key1': 'val1', 'key2': 'val2'},
}
expected_kwargs = {
'name': 'some-name',
'hostname': 'some-uuid',
'command': None,
'mem_limit': '512m',
'environment': {'key1': 'val1', 'key2': 'val2'},
}
self._test_container_create(container_dict, expected_kwargs)
def test_encode_utf8_unicode(self):
image = 'some_image:some_tag'
unicode_image = six.u(image)
utf8_image = self.conductor._encode_utf8(unicode_image)
self.assertEqual(unicode_image.encode('utf-8'), utf8_image)
@mock.patch.object(errors.APIError, '__str__')
def test_container_create_with_failure(self, mock_init):
mock_container = mock.MagicMock()
mock_container.image = 'test_image:some_tag'
mock_init.return_value = 'hit error'
self.mock_docker.pull = mock.Mock(
side_effect=errors.APIError('Error', '', ''))
self.assertRaises(exception.ContainerException,
self.conductor.container_create,
None, mock_container)
self.mock_docker.pull.assert_called_once_with(
'test_image',
tag='some_tag')
self.assertFalse(self.mock_docker.create_container.called)
mock_init.assert_called_with()
self.assertEqual(fields.ContainerStatus.ERROR,
mock_container.status)
def test_find_container_by_name_not_found(self):
mock_docker = mock.MagicMock()
fake_response = mock.MagicMock()
fake_response.content = 'not_found'
fake_response.status_code = 404
mock_docker.list_instances.side_effect = errors.APIError(
'not_found', fake_response)
ret = self.conductor._find_container_by_name(mock_docker, '1')
self.assertEqual({}, ret)
@mock.patch.object(docker_conductor.Handler, '_find_container_by_name')
def test_container_delete(self, mock_find_container):
mock_container_uuid = 'd545a92d-609a-428f-8edb-16b02ad20ca1'
mock_docker_id = '2703ef2b705d'
mock_find_container.return_value = mock_docker_id
self.conductor.container_delete(None, mock_container_uuid)
self.mock_docker.remove_container.assert_called_once_with(
mock_docker_id)
mock_find_container.assert_called_once_with(self.mock_docker,
mock_container_uuid)
@mock.patch.object(docker_conductor.Handler, '_find_container_by_name')
def test_container_delete_with_container_not_exist(self,
mock_find_container):
mock_container_uuid = 'd545a92d-609a-428f-8edb-16b02ad20ca1'
mock_docker_id = {}
mock_find_container.return_value = mock_docker_id
res = self.conductor.container_delete(None, mock_container_uuid)
self.assertIsNone(res)
self.assertFalse(self.mock_docker.remove_container.called)
mock_find_container.assert_called_once_with(self.mock_docker,
mock_container_uuid)
@mock.patch.object(errors.APIError, '__str__')
@mock.patch.object(docker_conductor.Handler, '_find_container_by_name')
def test_container_delete_with_failure(self, mock_find_container,
mock_init):
mock_container_uuid = 'd545a92d-609a-428f-8edb-16b02ad20ca1'
mock_docker_id = '2703ef2b705d'
mock_find_container.return_value = mock_docker_id
mock_init.return_value = 'hit error'
self.mock_docker.remove_container = mock.Mock(
side_effect=errors.APIError('Error', '', ''))
self.assertRaises(exception.ContainerException,
self.conductor.container_delete,
None, mock_container_uuid)
self.mock_docker.remove_container.assert_called_once_with(
mock_docker_id)
mock_find_container.assert_called_once_with(self.mock_docker,
mock_container_uuid)
mock_init.assert_called_with()
@mock.patch.object(objects.Container, 'get_by_uuid')
@mock.patch.object(docker_conductor.Handler, '_find_container_by_name')
def test_container_action(self, mock_find_container, mock_get_by_uuid):
mock_container = mock.MagicMock()
mock_get_by_uuid.return_value = mock_container
mock_container_uuid = 'd545a92d-609a-428f-8edb-16b02ad20ca1'
mock_docker_id = '2703ef2b705d'
mock_find_container.return_value = mock_docker_id
self.conductor._container_action(None, mock_container_uuid,
'fake-status', 'fake-func')
self.assertEqual('fake-status', mock_container.status)
def _test_container(self, action, docker_func_name, expected_status,
mock_find_container, mock_get_by_uuid):
mock_container = mock.MagicMock()
mock_get_by_uuid.return_value = mock_container
mock_container_uuid = 'd545a92d-609a-428f-8edb-16b02ad20ca1'
mock_docker_id = '2703ef2b705d'
mock_find_container.return_value = mock_docker_id
action_func = getattr(self.conductor, action)
action_func(None, mock_container_uuid)
docker_func = getattr(self.mock_docker, docker_func_name)
docker_func.assert_called_once_with(mock_docker_id)
mock_find_container.assert_called_once_with(self.mock_docker,
mock_container_uuid)
self.assertEqual(expected_status, mock_container.status)
@mock.patch.object(errors.APIError, '__str__')
def _test_container_with_failure(
self, action, docker_func_name, mock_find_container, mock_init):
mock_container_uuid = 'd545a92d-609a-428f-8edb-16b02ad20ca1'
mock_docker_id = '2703ef2b705d'
mock_find_container.return_value = mock_docker_id
mock_init.return_value = 'hit error'
setattr(self.mock_docker, docker_func_name, mock.Mock(
side_effect=errors.APIError('Error', '', '')))
self.assertRaises(exception.ContainerException,
getattr(self.conductor, action),
None, mock_container_uuid)
docker_func = getattr(self.mock_docker, docker_func_name)
docker_func.assert_called_once_with(mock_docker_id)
mock_find_container.assert_called_once_with(self.mock_docker,
mock_container_uuid)
mock_init.assert_called_with()
@mock.patch.object(objects.Container, 'get_by_uuid')
@mock.patch.object(docker_conductor.Handler, '_find_container_by_name')
def test_container_reboot(self, mock_find_container, mock_get_by_uuid):
self._test_container(
'container_reboot', 'restart', fields.ContainerStatus.RUNNING,
mock_find_container, mock_get_by_uuid)
@mock.patch.object(docker_conductor.Handler, '_find_container_by_name')
def test_container_reboot_with_failure(self, mock_find_container):
self._test_container_with_failure(
'container_reboot', 'restart', mock_find_container)
@mock.patch.object(objects.Container, 'get_by_uuid')
@mock.patch.object(docker_conductor.Handler, '_find_container_by_name')
def test_container_start(self, mock_find_container, mock_get_by_uuid):
self._test_container(
'container_start', 'start', fields.ContainerStatus.RUNNING,
mock_find_container, mock_get_by_uuid)
@mock.patch.object(docker_conductor.Handler, '_find_container_by_name')
def test_container_start_with_failure(self, mock_find_container):
self._test_container_with_failure(
'container_start', 'start', mock_find_container)
@mock.patch.object(objects.Container, 'get_by_uuid')
@mock.patch.object(docker_conductor.Handler, '_find_container_by_name')
def test_container_stop(self, mock_find_container, mock_get_by_uuid):
self._test_container(
'container_stop', 'stop', fields.ContainerStatus.STOPPED,
mock_find_container, mock_get_by_uuid)
@mock.patch.object(docker_conductor.Handler, '_find_container_by_name')
def test_container_stop_with_failure(self, mock_find_container):
self._test_container_with_failure(
'container_stop', 'stop', mock_find_container)
@mock.patch.object(objects.Container, 'get_by_uuid')
@mock.patch.object(docker_conductor.Handler, '_find_container_by_name')
def test_container_pause(self, mock_find_container, mock_get_by_uuid):
self._test_container(
'container_pause', 'pause', fields.ContainerStatus.PAUSED,
mock_find_container, mock_get_by_uuid)
@mock.patch.object(docker_conductor.Handler, '_find_container_by_name')
def test_container_pause_with_failure(self, mock_find_container):
self._test_container_with_failure(
'container_pause', 'pause', mock_find_container)
@mock.patch.object(objects.Container, 'get_by_uuid')
@mock.patch.object(docker_conductor.Handler, '_find_container_by_name')
def test_container_unpause(self, mock_find_container, mock_get_by_uuid):
self._test_container(
'container_unpause', 'unpause', fields.ContainerStatus.RUNNING,
mock_find_container, mock_get_by_uuid)
@mock.patch.object(docker_conductor.Handler, '_find_container_by_name')
def test_container_unpause_with_failure(self, mock_find_container):
self._test_container_with_failure(
'container_unpause', 'unpause', mock_find_container)
def _test_container_show(
self, mock_find_container, mock_get_by_uuid, container_detail=None,
expected_status=None, mock_docker_id='2703ef2b705d'):
mock_container = mock.MagicMock()
mock_get_by_uuid.return_value = mock_container
mock_container_uuid = 'd545a92d-609a-428f-8edb-16b02ad20ca1'
mock_find_container.return_value = mock_docker_id
if container_detail is not None:
self.mock_docker.inspect_container.return_value = container_detail
self.conductor.container_show(None, mock_container_uuid)
if mock_docker_id:
self.mock_docker.inspect_container.assert_called_once_with(
mock_docker_id)
mock_find_container.assert_called_once_with(self.mock_docker,
mock_container_uuid)
if expected_status is not None:
self.assertEqual(expected_status, mock_container.status)
@mock.patch.object(objects.Container, 'get_by_uuid')
@mock.patch.object(docker_conductor.Handler, '_find_container_by_name')
def test_container_show(self, mock_find_container, mock_get_by_uuid):
self._test_container_show(mock_find_container, mock_get_by_uuid)
@mock.patch.object(objects.Container, 'get_by_uuid')
@mock.patch.object(docker_conductor.Handler, '_find_container_by_name')
def test_container_show_with_running_state(self, mock_find_container,
mock_get_by_uuid):
mock_container_detail = {'State': {'Error': '',
'Running': True,
'Paused': False}}
self._test_container_show(
mock_find_container, mock_get_by_uuid, mock_container_detail,
fields.ContainerStatus.RUNNING)
@mock.patch.object(objects.Container, 'get_by_uuid')
@mock.patch.object(docker_conductor.Handler, '_find_container_by_name')
def test_container_show_with_stop_state(self, mock_find_container,
mock_get_by_uuid):
mock_container_detail = {'State': {'Error': '',
'Running': False,
'Paused': False}}
self._test_container_show(
mock_find_container, mock_get_by_uuid, mock_container_detail,
fields.ContainerStatus.STOPPED)
@mock.patch.object(objects.Container, 'get_by_uuid')
@mock.patch.object(docker_conductor.Handler, '_find_container_by_name')
def test_container_show_with_pause_state(self, mock_find_container,
mock_get_by_uuid):
mock_container_detail = {'State': {'Error': '',
'Running': True,
'Paused': True}}
self._test_container_show(
mock_find_container, mock_get_by_uuid, mock_container_detail,
fields.ContainerStatus.PAUSED)
@mock.patch.object(objects.Container, 'get_by_uuid')
@mock.patch.object(docker_conductor.Handler, '_find_container_by_name')
def test_container_show_with_error_status(self, mock_find_container,
mock_get_by_uuid):
mock_container_detail = {'State': {'Error': True,
'Running': False,
'Paused': False}}
self._test_container_show(
mock_find_container, mock_get_by_uuid, mock_container_detail,
fields.ContainerStatus.ERROR)
def _test_container_show_with_failure(
self, mock_find_container, mock_get_by_uuid, error,
assert_raise=True, expected_status=None):
mock_container = mock.MagicMock()
mock_get_by_uuid.return_value = mock_container
mock_container_uuid = 'd545a92d-609a-428f-8edb-1d6b02ad20ca1'
mock_docker_id = '2703ef2b705d'
mock_find_container.return_value = mock_docker_id
with mock.patch.object(errors.APIError, '__str__',
return_value=error) as mock_init:
self.mock_docker.inspect_container = mock.Mock(
side_effect=errors.APIError('Error', '', ''))
if assert_raise:
self.assertRaises(exception.ContainerException,
self.conductor.container_show,
None, mock_container_uuid)
else:
self.conductor.container_show(None, mock_container_uuid)
self.mock_docker.inspect_container.assert_called_once_with(
mock_docker_id)
mock_find_container.assert_called_once_with(self.mock_docker,
mock_container_uuid)
mock_init.assert_called_with()
if expected_status is not None:
self.assertEqual(expected_status, mock_container.status)
@mock.patch.object(objects.Container, 'get_by_uuid')
@mock.patch.object(docker_conductor.Handler, '_find_container_by_name')
def test_container_show_with_failure(self, mock_find_container,
mock_get_by_uuid):
self._test_container_show_with_failure(
mock_find_container, mock_get_by_uuid, error='hit error')
@mock.patch.object(objects.Container, 'get_by_uuid')
@mock.patch.object(docker_conductor.Handler, '_find_container_by_name')
def test_container_show_with_not_found(self, mock_find_container,
mock_get_by_uuid):
self._test_container_show_with_failure(
mock_find_container, mock_get_by_uuid, error='404 error',
assert_raise=False, expected_status=fields.ContainerStatus.ERROR)
@mock.patch.object(objects.Container, 'get_by_uuid')
@mock.patch.object(docker_conductor.Handler, '_find_container_by_name')
def test_container_show_with_not_found_from_docker(self,
mock_find_container,
mock_get_by_uuid):
self._test_container_show(
mock_find_container, mock_get_by_uuid, mock_docker_id={},
expected_status=fields.ContainerStatus.ERROR)
def _test_container_exec(self, mock_find_container, docker_version='1.2.2',
deprecated=False):
mock_container_uuid = 'd545a92d-609a-428f-8edb-16b02ad20ca1'
mock_docker_id = '2703ef2b705d'
docker.version = docker_version
mock_find_container.return_value = mock_docker_id
mock_create_res = mock.MagicMock()
self.mock_docker.exec_create.return_value = mock_create_res
self.conductor.container_exec(None, mock_container_uuid, 'ls')
if deprecated:
self.mock_docker.execute.assert_called_once_with(
mock_docker_id, 'ls')
else:
self.mock_docker.exec_create.assert_called_once_with(
mock_docker_id, 'ls', True, True, False)
self. mock_docker.exec_start.assert_called_once_with(
mock_create_res, False, False, False)
mock_find_container.assert_called_once_with(self.mock_docker,
mock_container_uuid)
@mock.patch.object(docker_conductor.Handler, '_find_container_by_name')
def test_container_exec(self, mock_find_container):
self._test_container_exec(mock_find_container)
@mock.patch.object(docker_conductor.Handler, '_find_container_by_name')
def test_container_exec_deprecated(self, mock_find_container):
self._test_container_exec(
mock_find_container, docker_version='0.7.0', deprecated=True)