From d7401499453d5ed19e88ec988ee35579b51f00a1 Mon Sep 17 00:00:00 2001 From: Hongbin Lu Date: Sun, 31 Jul 2016 21:12:35 -0500 Subject: [PATCH] Implement Zun compute * Implement compute manager * Introduce zun/container folder for containers (equivalent to nova/virt in Nova) * Have a docker driver in zun/container/docker * Remove conductor since it is not needed anymore Change-Id: I627f7ba1c40584178e1526947088c847554a82af --- zun/cmd/conductor.py | 48 ------ zun/common/exception.py | 4 + zun/compute/api.py | 36 ++--- zun/compute/manager.py | 133 +++++++++++++--- zun/conductor/api.py | 33 ---- zun/conductor/config.py | 28 ---- zun/conductor/handlers/default.py | 24 --- zun/{conductor => container}/__init__.py | 0 .../handlers => container/docker}/__init__.py | 0 zun/container/docker/driver.py | 142 +++++++++++++++++ zun/container/docker/utils.py | 146 ++++++++++++++++++ zun/container/driver.py | 123 +++++++++++++++ ...38_add_container_id_column_to_container.py | 34 ++++ zun/db/sqlalchemy/api.py | 10 +- zun/db/sqlalchemy/models.py | 3 +- zun/objects/container.py | 4 +- zun/opts.py | 4 +- zun/tests/unit/db/test_container.py | 12 +- zun/tests/unit/db/utils.py | 1 + zun/tests/unit/objects/test_container.py | 5 +- 20 files changed, 601 insertions(+), 189 deletions(-) delete mode 100644 zun/cmd/conductor.py delete mode 100644 zun/conductor/api.py delete mode 100644 zun/conductor/config.py delete mode 100644 zun/conductor/handlers/default.py rename zun/{conductor => container}/__init__.py (100%) rename zun/{conductor/handlers => container/docker}/__init__.py (100%) create mode 100644 zun/container/docker/driver.py create mode 100644 zun/container/docker/utils.py create mode 100644 zun/container/driver.py create mode 100644 zun/db/sqlalchemy/alembic/versions/5971a6844738_add_container_id_column_to_container.py diff --git a/zun/cmd/conductor.py b/zun/cmd/conductor.py deleted file mode 100644 index 17fd46a33..000000000 --- a/zun/cmd/conductor.py +++ /dev/null @@ -1,48 +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. - -"""Starter script for the Zun conductor service.""" - -import os -import sys - -from oslo_config import cfg -from oslo_log import log as logging -from oslo_service import service - -from zun.common.i18n import _LI -from zun.common import rpc_service -from zun.common import service as zun_service -from zun.common import short_id -from zun.conductor.handlers import default as default_handler - -LOG = logging.getLogger(__name__) - - -def main(): - zun_service.prepare_service(sys.argv) - - LOG.info(_LI('Starting server in PID %s'), os.getpid()) - cfg.CONF.log_opt_values(LOG, logging.DEBUG) - - cfg.CONF.import_opt('topic', 'zun.conductor.config', group='conductor') - - conductor_id = short_id.generate_id() - endpoints = [ - default_handler.Handler(), - ] - - server = rpc_service.Service.create(cfg.CONF.conductor.topic, - conductor_id, endpoints, - binary='zun-conductor') - launcher = service.launch(cfg.CONF, server) - launcher.wait() diff --git a/zun/common/exception.py b/zun/common/exception.py index 6069c56de..03dd817b3 100644 --- a/zun/common/exception.py +++ b/zun/common/exception.py @@ -337,5 +337,9 @@ class ConfigInvalid(ZunException): message = _("Invalid configuration file. %(error_msg)s") +class ContainerNotFound(HTTPNotFound): + message = _("Container %(container)s could not be found.") + + class ContainerAlreadyExists(ResourceExists): message = _("A container with UUID %(uuid)s already exists.") diff --git a/zun/compute/api.py b/zun/compute/api.py index 6313cf53c..0ea46d912 100644 --- a/zun/compute/api.py +++ b/zun/compute/api.py @@ -36,29 +36,29 @@ class API(rpc_service.API): def container_create(self, context, container): return self._call('container_create', container=container) - def container_delete(self, context, container_uuid): - return self._call('container_delete', container_uuid=container_uuid) + def container_delete(self, context, container): + return self._call('container_delete', container=container) - def container_show(self, context, container_uuid): - return self._call('container_show', container_uuid=container_uuid) + def container_show(self, context, container): + return self._call('container_show', container=container) - def container_reboot(self, context, container_uuid): - return self._call('container_reboot', container_uuid=container_uuid) + def container_reboot(self, context, container): + return self._call('container_reboot', container=container) - def container_stop(self, context, container_uuid): - return self._call('container_stop', container_uuid=container_uuid) + def container_stop(self, context, container): + return self._call('container_stop', container=container) - def container_start(self, context, container_uuid): - return self._call('container_start', container_uuid=container_uuid) + def container_start(self, context, container): + return self._call('container_start', container=container) - def container_pause(self, context, container_uuid): - return self._call('container_pause', container_uuid=container_uuid) + def container_pause(self, context, container): + return self._call('container_pause', container=container) - def container_unpause(self, context, container_uuid): - return self._call('container_unpause', container_uuid=container_uuid) + def container_unpause(self, context, container): + return self._call('container_unpause', container=container) - def container_logs(self, context, container_uuid): - return self._call('container_logs', container_uuid=container_uuid) + def container_logs(self, context, container): + return self._call('container_logs', container=container) - def container_exec(self, context, container_uuid, command): - return self._call('container_exec', container_uuid=container_uuid) + def container_exec(self, context, container, command): + return self._call('container_exec', container=container) diff --git a/zun/compute/manager.py b/zun/compute/manager.py index ec959ae94..125ce79ef 100644 --- a/zun/compute/manager.py +++ b/zun/compute/manager.py @@ -12,39 +12,132 @@ # License for the specific language governing permissions and limitations # under the License. +from oslo_log import log as logging + +from zun.common.i18n import _LE +from zun.container import driver + + +LOG = logging.getLogger(__name__) + class Manager(object): '''Manages the running containers.''' - def __init__(self): + def __init__(self, container_driver=None): super(Manager, self).__init__() + self.driver = driver.load_container_driver(container_driver) def container_create(self, context, container): - pass + LOG.debug('Creating container...', context=context, + container=container) + try: + container = self.driver.create(container) + return container + except Exception as e: + LOG.exception(_LE("Unexpected exception: %s,"), str(e)) + raise - def container_delete(self, context, container_uuid): - pass + def container_delete(self, context, container): + LOG.debug('Deleting container...', context=context, + container=container.uuid) + try: + self.driver.delete(container) + container.destroy() + return container + except Exception as e: + LOG.exception(_LE("Unexpected exception: %s,"), str(e)) + raise - def container_show(self, context, container_uuid): - pass + def container_list(self, context): + LOG.debug('Showing container...', context=context) + try: + return self.driver.list() + except Exception as e: + LOG.exception(_LE("Unexpected exception: %s,"), str(e)) + raise - def container_reboot(self, context, container_uuid): - pass + def container_show(self, context, container): + LOG.debug('Showing container...', context=context, + container=container.uuid) + try: + container = self.driver.show(container) + container.save() + return container + except Exception as e: + LOG.exception(_LE("Unexpected exception: %s,"), str(e)) + raise - def container_stop(self, context, container_uuid): - pass + def container_reboot(self, context, container): + LOG.debug('Rebooting container...', context=context, + container=container) + try: + container = self.driver.reboot(container) + container.save() + return container + except Exception as e: + LOG.exception(_LE("Unexpected exception: %s,"), str(e)) + raise - def container_start(self, context, container_uuid): - pass + def container_stop(self, context, container): + LOG.debug('Stopping container...', context=context, + container=container) + try: + container = self.driver.stop(container) + container.save() + return container + except Exception as e: + LOG.exception(_LE("Unexpected exception: %s,"), str(e)) + raise - def container_pause(self, context, container_uuid): - pass + def container_start(self, context, container): + LOG.debug('Starting container...', context=context, + container=container.uuid) + try: + container = self.driver.start(container) + container.save() + return container + except Exception as e: + LOG.exception(_LE("Unexpected exception: %s,"), str(e)) + raise - def container_unpause(self, context, container_uuid): - pass + def container_pause(self, context, container): + LOG.debug('Pausing container...', context=context, + container=container) + try: + container = self.driver.pause(container) + container.save() + return container + except Exception as e: + LOG.exception(_LE("Unexpected exception: %s,"), str(e)) + raise - def container_logs(self, context, container_uuid): - pass + def container_unpause(self, context, container): + LOG.debug('Unpausing container...', context=context, + container=container) + try: + container = self.driver.unpause(container) + container.save() + return container + except Exception as e: + LOG.exception(_LE("Unexpected exception: %s,"), str(e)) + raise - def container_exec(self, context, container_uuid, command): - pass + def container_logs(self, context, container): + LOG.debug('Showing container logs...', context=context, + container=container) + try: + return self.driver.show_logs(container) + except Exception as e: + LOG.exception(_LE("Unexpected exception: %s,"), str(e)) + raise + + def container_exec(self, context, container, command): + # TODO(hongbin): support exec command interactively + LOG.debug('Executing command in container...', context=context, + container=container) + try: + return self.driver.execute(container) + except Exception as e: + LOG.exception(_LE("Unexpected exception: %s,"), str(e)) + raise diff --git a/zun/conductor/api.py b/zun/conductor/api.py deleted file mode 100644 index 8a8eeb650..000000000 --- a/zun/conductor/api.py +++ /dev/null @@ -1,33 +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. - -"""API for interfacing with Zun Backend.""" -from oslo_config import cfg - -from zun.common import rpc_service - - -# The Backend API class serves as a AMQP client for communicating -# on a topic exchange specific to the conductors. This allows the ReST -# API to trigger operations on the conductors - -class API(rpc_service.API): - def __init__(self, transport=None, context=None, topic=None): - if topic is None: - cfg.CONF.import_opt('topic', 'zun.conductor.config', - group='conductor') - super(API, self).__init__(transport, context, - topic=cfg.CONF.conductor.topic) - - # NOTE(vivek): Add all APIs here - def container_get(self, uuid): - return self._call('container_get', uuid=uuid) diff --git a/zun/conductor/config.py b/zun/conductor/config.py deleted file mode 100644 index ac4ad2e0e..000000000 --- a/zun/conductor/config.py +++ /dev/null @@ -1,28 +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. - -"""Config options for Zun Backend service.""" - - -from oslo_config import cfg - -SERVICE_OPTS = [ - cfg.StrOpt('topic', - default='zun-conductor', - help='The queue to add conductor tasks to.'), -] - -opt_group = cfg.OptGroup( - name='conductor', - title='Options for the zun-conductor service') -cfg.CONF.register_group(opt_group) -cfg.CONF.register_opts(SERVICE_OPTS, opt_group) diff --git a/zun/conductor/handlers/default.py b/zun/conductor/handlers/default.py deleted file mode 100644 index d112033d9..000000000 --- a/zun/conductor/handlers/default.py +++ /dev/null @@ -1,24 +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. - -"""Zun Conductor default handler.""" - - -# These are the database operations - They are executed by the conductor -# service. API calls via AMQP trigger the handlers to be called. - -class Handler(object): - def __init__(self): - super(Handler, self).__init__() - - def container_get(uuid): - pass diff --git a/zun/conductor/__init__.py b/zun/container/__init__.py similarity index 100% rename from zun/conductor/__init__.py rename to zun/container/__init__.py diff --git a/zun/conductor/handlers/__init__.py b/zun/container/docker/__init__.py similarity index 100% rename from zun/conductor/handlers/__init__.py rename to zun/container/docker/__init__.py diff --git a/zun/container/docker/driver.py b/zun/container/docker/driver.py new file mode 100644 index 000000000..61f57ff36 --- /dev/null +++ b/zun/container/docker/driver.py @@ -0,0 +1,142 @@ +# 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 docker import errors +import six + +from oslo_config import cfg +from oslo_log import log as logging + +from zun.container.docker import utils as docker_utils +from zun.container import driver +from zun.objects import fields + + +LOG = logging.getLogger(__name__) +CONF = cfg.CONF + + +class DockerDriver(driver.ContainerDriver): + '''Implementation of container drivers for Docker.''' + + def __init__(self): + super(DockerDriver, self).__init__() + + def create(self, container): + with docker_utils.docker_client() as docker: + name = container.name + 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) + 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'] = {'mem_limit': + container.memory} + else: + kwargs['mem_limit'] = container.memory + + response = docker.create_container(image, **kwargs) + container.container_id = response['Id'] + container.status = fields.ContainerStatus.STOPPED + except errors.APIError as e: + container.status = fields.ContainerStatus.ERROR + container.status_reason = six.text_type(e) + raise + container.save() + return container + + def delete(self, container): + with docker_utils.docker_client() as docker: + return docker.remove_container(container.container_id) + + def list(self): + with docker_utils.docker_client() as docker: + return docker.list_instances() + + def show(self, container): + with docker_utils.docker_client() as docker: + try: + result = docker.inspect_container(container.uuid) + 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 + return container + except errors.APIError as api_error: + if '404' in str(api_error): + container.status = fields.ContainerStatus.ERROR + return container + raise + + def reboot(self, container): + with docker_utils.docker_client() as docker: + docker.restart(container.container_id) + container.status = fields.ContainerStatus.RUNNING + return container + + def stop(self, container): + with docker_utils.docker_client() as docker: + docker.stop(container.container_id) + container.status = fields.ContainerStatus.STOPPED + return container + + def start(self, container): + with docker_utils.docker_client() as docker: + docker.start(container.container_id) + container.status = fields.ContainerStatus.RUNNING + return container + + def pause(self, container): + with docker_utils.docker_client() as docker: + docker.pause(container.container_id) + container.status = fields.ContainerStatus.PAUSED + return container + + def unpause(self, container): + with docker_utils.docker_client() as docker: + docker.unpause(container.container_id) + container.status = fields.ContainerStatus.RUNNING + return container + + def show_logs(self, container): + with docker_utils.docker_client() as docker: + return docker.get_container_logs(container.container_id) + + def execute(self, container, command): + with docker_utils.docker_client() as docker: + if docker_utils.is_docker_library_version_atleast('1.2.0'): + create_res = docker.exec_create( + container.container_id, command, True, True, False) + exec_output = docker.exec_start(create_res, False, False, + False) + else: + exec_output = docker.execute(container.container_id, command) + return exec_output + + def _encode_utf8(self, value): + if six.PY2 and not isinstance(value, unicode): + value = unicode(value) + return value.encode('utf-8') diff --git a/zun/container/docker/utils.py b/zun/container/docker/utils.py new file mode 100644 index 000000000..503b7422b --- /dev/null +++ b/zun/container/docker/utils.py @@ -0,0 +1,146 @@ +# 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 contextlib + +import docker +from docker import client +from docker import tls +from docker.utils import utils +from oslo_config import cfg + + +docker_opts = [ + cfg.StrOpt('docker_remote_api_version', + default='1.20', + help='Docker remote api version. Override it according to ' + 'specific docker api version in your environment.'), + cfg.IntOpt('default_timeout', + default=60, + help='Default timeout in seconds for docker client ' + 'operations.'), + cfg.StrOpt('api_url', + default='unix:///var/run/docker.sock', + help='API endpoint of docker daemon'), + cfg.BoolOpt('api_insecure', + default=False, + help='If set, ignore any SSL validation issues'), + cfg.StrOpt('ca_file', + help='Location of CA certificates file for ' + 'securing docker api requests (tlscacert).'), + cfg.StrOpt('cert_file', + help='Location of TLS certificate file for ' + 'securing docker api requests (tlscert).'), + cfg.StrOpt('key_file', + help='Location of TLS private key file for ' + 'securing docker api requests (tlskey).'), +] + +CONF = cfg.CONF +CONF.register_opts(docker_opts, 'docker') + + +def parse_docker_image(image): + image_parts = image.split(':', 1) + + image_repo = image_parts[0] + image_tag = None + + if len(image_parts) > 1: + image_tag = image_parts[1] + + return image_repo, image_tag + + +def is_docker_library_version_atleast(version): + if utils.compare_version(docker.version, version) <= 0: + return True + return False + + +def is_docker_api_version_atleast(docker, version): + if utils.compare_version(docker.version()['ApiVersion'], version) <= 0: + return True + return False + + +@contextlib.contextmanager +def docker_client(): + client_kwargs = dict() + if not CONF.docker.api_insecure: + client_kwargs['ca_cert'] = CONF.docker.ca_file + client_kwargs['client_key'] = CONF.docker.key_file + client_kwargs['client_cert'] = CONF.docker.key_file + + yield DockerHTTPClient( + CONF.docker.api_url, + CONF.docker.docker_remote_api_version, + CONF.docker.default_timeout, + **client_kwargs + ) + + +class DockerHTTPClient(client.Client): + def __init__(self, url=CONF.docker.api_url, + ver=CONF.docker.docker_remote_api_version, + timeout=CONF.docker.default_timeout, + ca_cert=None, + client_key=None, + client_cert=None): + + if ca_cert and client_key and client_cert: + ssl_config = tls.TLSConfig( + client_cert=(client_cert, client_key), + verify=ca_cert, + assert_hostname=False, + ) + else: + ssl_config = False + + super(DockerHTTPClient, self).__init__( + base_url=url, + version=ver, + timeout=timeout, + tls=ssl_config + ) + + def list_instances(self, inspect=False): + """List all containers.""" + res = [] + for container in self.containers(all=True): + info = self.inspect_container(container['Id']) + if not info: + continue + if inspect: + res.append(info) + else: + res.append(info['Config'].get('Hostname')) + return res + + def pause(self, container): + """Pause a running container.""" + if isinstance(container, dict): + container = container.get('Id') + url = self._url('/containers/{0}/pause'.format(container)) + res = self._post(url) + self._raise_for_status(res) + + def unpause(self, container): + """Unpause a paused container.""" + if isinstance(container, dict): + container = container.get('Id') + url = self._url('/containers/{0}/unpause'.format(container)) + res = self._post(url) + self._raise_for_status(res) + + def get_container_logs(self, docker_id): + """Fetch the logs of a container.""" + return self.logs(docker_id) diff --git a/zun/container/driver.py b/zun/container/driver.py new file mode 100644 index 000000000..447f6fc67 --- /dev/null +++ b/zun/container/driver.py @@ -0,0 +1,123 @@ +# 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 sys + +from oslo_config import cfg +from oslo_log import log as logging +from oslo_utils import importutils + +from zun.common.i18n import _LE +from zun.common.i18n import _LI + + +LOG = logging.getLogger(__name__) + +driver_opts = [ + cfg.StrOpt('container_driver', + default='docker.driver.DockerDriver', + help="""Defines which driver to use for controlling container. + +Possible values: + +* ``docker.driver.DockerDriver`` + +Services which consume this: + +* ``zun-compute`` + +Interdependencies to other options: + +* None +""") +] +CONF = cfg.CONF +CONF.register_opts(driver_opts) + + +def load_container_driver(container_driver=None): + """Load a container driver module. + + Load the container driver module specified by the container_driver + configuration option or, if supplied, the driver name supplied as an + argument. + :param container_driver: a container driver name to override the config opt + :returns: a ContainerDriver instance + """ + if not container_driver: + container_driver = CONF.container_driver + + if not container_driver: + LOG.error(_LE("Container driver option required, but not specified")) + sys.exit(1) + + LOG.info(_LI("Loading container driver '%s'"), container_driver) + try: + driver = importutils.import_object( + 'zun.container.%s' % container_driver) + if not isinstance(driver, ContainerDriver): + raise Exception(_('Expected driver of type: %s') % + str(ContainerDriver)) + + return driver + except ImportError: + LOG.exception(_LE("Unable to load the container driver")) + sys.exit(1) + + +class ContainerDriver(object): + '''Base class for container drivers.''' + + def create(self, container): + """Create a container.""" + raise NotImplementedError() + + def delete(self, container): + """Delete a container.""" + raise NotImplementedError() + + def list(self): + """List all containers.""" + raise NotImplementedError() + + def show(self, container): + """Show the details of a container.""" + raise NotImplementedError() + + def reboot(self, container): + """Reboot a container.""" + raise NotImplementedError() + + def stop(self, container): + """Stop a container.""" + raise NotImplementedError() + + def start(self, container): + """Start a container.""" + raise NotImplementedError() + + def pause(self, container): + """Pause a container.""" + raise NotImplementedError() + + def unpause(self, container): + """Pause a container.""" + raise NotImplementedError() + + def show_logs(self, container): + """Show logs of a container.""" + raise NotImplementedError() + + def execute(self, container, command): + """Execute a command in a running container.""" + raise NotImplementedError() diff --git a/zun/db/sqlalchemy/alembic/versions/5971a6844738_add_container_id_column_to_container.py b/zun/db/sqlalchemy/alembic/versions/5971a6844738_add_container_id_column_to_container.py new file mode 100644 index 000000000..8795eb658 --- /dev/null +++ b/zun/db/sqlalchemy/alembic/versions/5971a6844738_add_container_id_column_to_container.py @@ -0,0 +1,34 @@ +# 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. + +"""add container_id column to container + +Revision ID: 5971a6844738 +Revises: 9fe371393a24 +Create Date: 2016-08-05 17:38:05.231740 + +""" + +# revision identifiers, used by Alembic. +revision = '5971a6844738' +down_revision = '9fe371393a24' +branch_labels = None +depends_on = None + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + op.add_column('container', + sa.Column('container_id', sa.String(length=255), + nullable=True)) diff --git a/zun/db/sqlalchemy/api.py b/zun/db/sqlalchemy/api.py index ce0a0cf76..c08e583bc 100644 --- a/zun/db/sqlalchemy/api.py +++ b/zun/db/sqlalchemy/api.py @@ -160,7 +160,7 @@ class Connection(api.Connection): try: return query.one() except NoResultFound: - raise exception.InstanceNotFound(container=container_id) + raise exception.ContainerNotFound(container=container_id) def get_container_by_uuid(self, context, container_uuid): query = model_query(models.Container) @@ -169,7 +169,7 @@ class Connection(api.Connection): try: return query.one() except NoResultFound: - raise exception.InstanceNotFound(container=container_uuid) + raise exception.ContainerNotFound(container=container_uuid) def get_container_by_name(self, context, container_name): query = model_query(models.Container) @@ -178,7 +178,7 @@ class Connection(api.Connection): try: return query.one() except NoResultFound: - raise exception.InstanceNotFound(container=container_name) + raise exception.ContainerNotFound(container=container_name) except MultipleResultsFound: raise exception.Conflict('Multiple containers exist with same ' 'name. Please use the container uuid ' @@ -191,7 +191,7 @@ class Connection(api.Connection): query = add_identity_filter(query, container_id) count = query.delete() if count != 1: - raise exception.InstanceNotFound(container_id) + raise exception.ContainerNotFound(container_id) def update_container(self, container_id, values): # NOTE(dtantsur): this can lead to very strange errors @@ -209,7 +209,7 @@ class Connection(api.Connection): try: ref = query.with_lockmode('update').one() except NoResultFound: - raise exception.InstanceNotFound(container=container_id) + raise exception.ContainerNotFound(container=container_id) if 'provision_state' in values: values['provision_updated_at'] = timeutils.utcnow() diff --git a/zun/db/sqlalchemy/models.py b/zun/db/sqlalchemy/models.py index d3b9277de..4ecebe3ba 100644 --- a/zun/db/sqlalchemy/models.py +++ b/zun/db/sqlalchemy/models.py @@ -1,5 +1,3 @@ -# Copyright 2013 Hewlett-Packard Development Company, L.P. -# # 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 @@ -127,6 +125,7 @@ class Container(Base): project_id = Column(String(255)) user_id = Column(String(255)) uuid = Column(String(36)) + container_id = Column(String(36)) name = Column(String(255)) image = Column(String(255)) command = Column(String(255)) diff --git a/zun/objects/container.py b/zun/objects/container.py index 9bd0f3ccb..1284769c0 100644 --- a/zun/objects/container.py +++ b/zun/objects/container.py @@ -21,12 +21,14 @@ from zun.objects import fields as z_fields class Container(base.ZunPersistentObject, base.ZunObject, base.ZunObjectDictCompat): # Version 1.0: Initial version - VERSION = '1.0' + # Version 1.1: Add container_id column + VERSION = '1.1' dbapi = dbapi.get_instance() fields = { 'id': fields.IntegerField(), + 'container_id': fields.StringField(nullable=True), 'uuid': fields.StringField(nullable=True), 'name': fields.StringField(nullable=True), 'project_id': fields.StringField(nullable=True), diff --git a/zun/opts.py b/zun/opts.py index 733b241f6..0288e8bf2 100644 --- a/zun/opts.py +++ b/zun/opts.py @@ -16,7 +16,7 @@ import zun.api.app import zun.common.keystone import zun.common.rpc_service import zun.common.service -import zun.conductor.config +import zun.compute.config def list_opts(): @@ -27,6 +27,6 @@ def list_opts(): zun.common.service.service_opts, )), ('api', zun.api.app.API_SERVICE_OPTS), - ('conductor', zun.conductor.config.SERVICE_OPTS), + ('compute', zun.compute.config.SERVICE_OPTS), ('keystone_auth', zun.common.keystone.keystone_auth_opts), ] diff --git a/zun/tests/unit/db/test_container.py b/zun/tests/unit/db/test_container.py index c2d78ea50..31f5d5af1 100644 --- a/zun/tests/unit/db/test_container.py +++ b/zun/tests/unit/db/test_container.py @@ -50,9 +50,9 @@ class DbContainerTestCase(base.DbTestCase): self.assertEqual(container.uuid, res.uuid) def test_get_container_that_does_not_exist(self): - self.assertRaises(exception.InstanceNotFound, + self.assertRaises(exception.ContainerNotFound, self.dbapi.get_container_by_id, self.context, 99) - self.assertRaises(exception.InstanceNotFound, + self.assertRaises(exception.ContainerNotFound, self.dbapi.get_container_by_uuid, self.context, uuidutils.generate_uuid()) @@ -110,19 +110,19 @@ class DbContainerTestCase(base.DbTestCase): def test_destroy_container(self): container = utils.create_test_container() self.dbapi.destroy_container(container.id) - self.assertRaises(exception.InstanceNotFound, + self.assertRaises(exception.ContainerNotFound, self.dbapi.get_container_by_id, self.context, container.id) def test_destroy_container_by_uuid(self): container = utils.create_test_container() self.dbapi.destroy_container(container.uuid) - self.assertRaises(exception.InstanceNotFound, + self.assertRaises(exception.ContainerNotFound, self.dbapi.get_container_by_uuid, self.context, container.uuid) def test_destroy_container_that_does_not_exist(self): - self.assertRaises(exception.InstanceNotFound, + self.assertRaises(exception.ContainerNotFound, self.dbapi.destroy_container, uuidutils.generate_uuid()) @@ -139,7 +139,7 @@ class DbContainerTestCase(base.DbTestCase): def test_update_container_not_found(self): container_uuid = uuidutils.generate_uuid() new_image = 'new-image' - self.assertRaises(exception.InstanceNotFound, + self.assertRaises(exception.ContainerNotFound, self.dbapi.update_container, container_uuid, {'image': new_image}) diff --git a/zun/tests/unit/db/utils.py b/zun/tests/unit/db/utils.py index 7f50a6086..6509cc2e9 100644 --- a/zun/tests/unit/db/utils.py +++ b/zun/tests/unit/db/utils.py @@ -19,6 +19,7 @@ def get_test_container(**kw): return { 'id': kw.get('id', 42), 'uuid': kw.get('uuid', 'ea8e2a25-2901-438d-8157-de7ffd68d051'), + 'container_id': kw.get('container_id', 'ddcb39a3fcec'), 'name': kw.get('name', 'container1'), 'project_id': kw.get('project_id', 'fake_project'), 'user_id': kw.get('user_id', 'fake_user'), diff --git a/zun/tests/unit/objects/test_container.py b/zun/tests/unit/objects/test_container.py index 7d32914f4..a89aa624c 100644 --- a/zun/tests/unit/objects/test_container.py +++ b/zun/tests/unit/objects/test_container.py @@ -71,14 +71,15 @@ class TestContainerObject(base.DbTestCase): with mock.patch.object(self.dbapi, 'list_container', autospec=True) as mock_get_list: mock_get_list.return_value = [self.fake_container] + filt = {'status': 'Running'} containers = objects.Container.list(self.context, - filters={'bay_uuid': 'uuid'}) + filters=filt) self.assertEqual(1, mock_get_list.call_count) self.assertThat(containers, HasLength(1)) self.assertIsInstance(containers[0], objects.Container) self.assertEqual(self.context, containers[0]._context) mock_get_list.assert_called_once_with(self.context, - filters={'bay_uuid': 'uuid'}, + filters=filt, limit=None, marker=None, sort_key=None, sort_dir=None)