From b7698ffe3b8f9b1a74b80aaa74ebb216b687554c Mon Sep 17 00:00:00 2001 From: Hongbin Lu Date: Sat, 12 Nov 2016 17:14:24 -0600 Subject: [PATCH] Implement the sandbox proposal - Part 1 After this commit, creating a container will (i) create a Docker container (with image kubernetes/pause) as a sandbox, and (ii) create another container by using the sandbox. Each driver must implement additional methods to create/delete/manage sandboxes. The driver interface is declared in zun/container/driver.py . The next step is to leverage Nova to create the sandbox container so that the container could have a neutron port. Partially-Implements: blueprint neutron-integration Change-Id: Id15d91f375a771b0d74c79d472d10fc97320d5e0 --- devstack/gate_hook.sh | 2 +- devstack/lib/zun | 20 +++++-- devstack/plugin.sh | 1 + zun/api/controllers/types.py | 14 +++++ zun/api/controllers/v1/containers.py | 15 ++++- zun/common/exception.py | 10 +++- zun/common/keystone.py | 5 +- zun/compute/manager.py | 45 +++++++++++++- zun/conf/container_driver.py | 4 +- zun/container/docker/driver.py | 59 +++++++++++++++++-- zun/container/driver.py | 26 +++++++- ...f7a4a33_add_meta_addresses_to_container.py | 39 ++++++++++++ zun/db/sqlalchemy/models.py | 2 + zun/objects/container.py | 8 ++- zun/objects/fields.py | 25 +++++++- zun/tests/tempest/api/clients.py | 11 ++++ zun/tests/tempest/api/test_containers.py | 33 +++-------- zun/tests/tempest/utils.py | 25 ++++++++ .../unit/compute/test_compute_manager.py | 56 ++++++++++-------- zun/tests/unit/container/fake_driver.py | 15 +++++ zun/tests/unit/db/utils.py | 11 ++++ 21 files changed, 353 insertions(+), 73 deletions(-) create mode 100644 zun/db/sqlalchemy/alembic/versions/4a0c4f7a4a33_add_meta_addresses_to_container.py create mode 100644 zun/tests/tempest/utils.py diff --git a/devstack/gate_hook.sh b/devstack/gate_hook.sh index d88721f96..aaaa5be8d 100755 --- a/devstack/gate_hook.sh +++ b/devstack/gate_hook.sh @@ -25,7 +25,7 @@ # userrc for us if nova service is not enabled, check # https://github.com/openstack-dev/devstack/blob/master/stack.sh#L1310 -OVERRIDE_ENABLED_SERVICES="dstat,key,mysql,rabbit,n-api,n-cond,n-cpu,n-crt,n-obj,n-sch,tempest" +OVERRIDE_ENABLED_SERVICES="dstat,key,mysql,rabbit,n-api,n-cond,n-cpu,n-crt,n-obj,n-sch,g-api,g-reg,tempest" export OVERRIDE_ENABLED_SERVICES $BASE/new/devstack-gate/devstack-vm-gate.sh diff --git a/devstack/lib/zun b/devstack/lib/zun index 568266850..f356b1ea5 100644 --- a/devstack/lib/zun +++ b/devstack/lib/zun @@ -64,8 +64,8 @@ else ZUN_BIN_DIR=$(get_python_exec_prefix) fi -DOCKER_GROUP=docker -DEFAULT_CONTAINER_DRIVER=docker +DOCKER_GROUP=${DOCKER_GROUP:-docker} +ZUN_DRIVER=${DEFAULT_ZUN_DRIVER:-docker} ETCD_VERSION=v3.0.13 if is_ubuntu; then @@ -109,11 +109,19 @@ function configure_zun { create_api_paste_conf - if [[ ${DEFAULT_CONTAINER_DRIVER} == "docker" ]]; then + if [[ ${ZUN_DRIVER} == "docker" ]]; then check_docker || install_docker fi } +# upload_sandbox_image() - Upload sandbox image to glance +function upload_sandbox_image { + if [[ ${ZUN_DRIVER} == "docker" ]]; then + sg docker "docker pull kubernetes/pause" + sg docker "docker save kubernetes/pause" | openstack image create kubernetes/pause --public --container-format docker --disk-format raw + fi +} + # create_zun_accounts() - Set up common required ZUN accounts # # Project User Roles @@ -314,7 +322,11 @@ function start_zun_api { # start_zun_compute() - Start Zun compute agent function start_zun_compute { echo "Start zun compute..." - run_process zun-compute "$ZUN_BIN_DIR/zun-compute" ${DOCKER_GROUP} + if [[ ${ZUN_DRIVER} == "docker" ]]; then + run_process zun-compute "$ZUN_BIN_DIR/zun-compute" ${DOCKER_GROUP} + else + run_process zun-compute "$ZUN_BIN_DIR/zun-compute" + fi } function start_zun_etcd { diff --git a/devstack/plugin.sh b/devstack/plugin.sh index 34ccc6742..acae9d6bd 100755 --- a/devstack/plugin.sh +++ b/devstack/plugin.sh @@ -32,6 +32,7 @@ if is_service_enabled zun-api zun-compute; then # Start the zun API and zun compute echo_summary "Starting zun" start_zun + upload_sandbox_image fi diff --git a/zun/api/controllers/types.py b/zun/api/controllers/types.py index 08ad5d46b..edddbfde4 100644 --- a/zun/api/controllers/types.py +++ b/zun/api/controllers/types.py @@ -256,6 +256,20 @@ class Dict(object): return value +class Json(object): + type_name = 'Json' + + @classmethod + def validate(self, value): + if value is None: + return None + + if not isinstance(value, dict): + raise exception.InvalidValue(value=value, type=self.type_name) + + return value + + class DateTime(object): type_name = "DateTime" diff --git a/zun/api/controllers/v1/containers.py b/zun/api/controllers/v1/containers.py index feb869a48..ecda34577 100644 --- a/zun/api/controllers/v1/containers.py +++ b/zun/api/controllers/v1/containers.py @@ -120,13 +120,16 @@ class Container(base.APIBase): 'labels': { 'validate': types.Dict(types.String, types.String).validate, }, + 'addresses': { + 'validate': types.Json.validate, + }, 'image_pull_policy': { 'validate': types.EnumType.validate, 'validate_args': { 'name': 'image_pull_policy', 'values': ['never', 'always', 'ifnotpresent'] } - } + }, } def __init__(self, **kwargs): @@ -138,7 +141,7 @@ class Container(base.APIBase): container.unset_fields_except([ 'uuid', 'name', 'image', 'command', 'status', 'cpu', 'memory', 'environment', 'task_state', 'workdir', 'ports', 'hostname', - 'labels', 'image_pull_policy', 'status_reason']) + 'labels', 'addresses', 'image_pull_policy', 'status_reason']) container.links = [link.Link.make_link( 'self', url, @@ -171,6 +174,14 @@ class Container(base.APIBase): ports=[80, 443], hostname='testhost', labels={'key1': 'val1', 'key2': 'val2'}, + addresses={ + 'private': [ + {'OS-EXT-IPS-MAC:mac_addr': 'fa:16:3e:04:da:76', + 'version': 4, + 'addr': '10.0.0.12', + 'OS-EXT-IPS:type': 'fixed'}, + ], + }, created_at=timeutils.utcnow(), updated_at=timeutils.utcnow()) return cls._convert_with_links(sample, 'http://localhost:9517', expand) diff --git a/zun/common/exception.py b/zun/common/exception.py index abf163612..1cdf35eeb 100644 --- a/zun/common/exception.py +++ b/zun/common/exception.py @@ -118,9 +118,9 @@ def wrap_controller_exception(func, func_server_error, func_client_error): # log the error message with its associated # correlation id log_correlation_id = uuidutils.generate_uuid() - LOG.error(_LE("%(correlation_id)s:%(excp)s") % - {'correlation_id': log_correlation_id, - 'excp': str(excp)}) + LOG.exception(_LE("%(correlation_id)s:%(excp)s") % + {'correlation_id': log_correlation_id, + 'excp': str(excp)}) # raise a client error with an obfuscated message return func_server_error(log_correlation_id, http_error_code) else: @@ -364,3 +364,7 @@ class InvalidStateException(ZunException): class DockerError(ZunException): message = _("Docker internal error: %(error_msg)s.") + + +class PollTimeOut(ZunException): + message = _("Polling request timed out.") diff --git a/zun/common/keystone.py b/zun/common/keystone.py index dc0c64000..00966bd3d 100644 --- a/zun/common/keystone.py +++ b/zun/common/keystone.py @@ -23,6 +23,7 @@ import zun.conf CONF = zun.conf.CONF CFG_GROUP = 'keystone_auth' +CFG_LEGACY_GROUP = 'keystone_authtoken' LOG = logging.getLogger(__name__) keystone_auth_opts = (ka_loading.get_auth_common_conf_options() + @@ -46,7 +47,9 @@ class KeystoneClientV3(object): @property def auth_url(self): - return CONF.keystone_authtoken.auth_uri.replace('v2.0', 'v3') + # FIXME(pauloewerton): auth_url should be retrieved from keystone_auth + # section by default + return CONF[CFG_LEGACY_GROUP].auth_uri.replace('v2.0', 'v3') @property def auth_token(self): diff --git a/zun/compute/manager.py b/zun/compute/manager.py index 52f286659..9235ee8b4 100644 --- a/zun/compute/manager.py +++ b/zun/compute/manager.py @@ -77,6 +77,23 @@ class Manager(object): LOG.debug('Creating container...', context=context, container=container) + container.task_state = fields.TaskState.SANDBOX_CREATING + container.save() + sandbox_id = None + sandbox_image = 'kubernetes/pause' + repo, tag = utils.parse_image_name(sandbox_image) + try: + image = image_driver.pull_image(context, repo, tag, 'ifnotpresent') + sandbox_id = self.driver.create_sandbox(context, container, + image=sandbox_image) + except Exception as e: + with excutils.save_and_reraise_exception(reraise=reraise): + LOG.exception(_LE("Unexpected exception: %s"), + six.text_type(e)) + self._fail_container(container, six.text_type(e)) + return + + self.driver.set_sandbox_id(container, sandbox_id) container.task_state = fields.TaskState.IMAGE_PULLING container.save() repo, tag = utils.parse_image_name(container.image) @@ -107,7 +124,9 @@ class Manager(object): container.task_state = fields.TaskState.CONTAINER_CREATING container.save() try: - container = self.driver.create(container, image) + container = self.driver.create(container, sandbox_id, image) + container.addresses = self._get_container_addresses(context, + container) container.task_state = None container.save() return container @@ -154,7 +173,6 @@ class Manager(object): if not force: self._validate_container_state(container, 'delete') self.driver.delete(container, force) - return container except exception.DockerError as e: LOG.error(_LE("Error occured while calling docker delete API: %s"), six.text_type(e)) @@ -163,6 +181,16 @@ class Manager(object): LOG.exception(_LE("Unexpected exception: %s"), str(e)) raise e + sandbox_id = self.driver.get_sandbox_id(container) + if sandbox_id: + try: + self.driver.delete_sandbox(context, sandbox_id) + except Exception as e: + LOG.exception(_LE("Unexpected exception: %s"), str(e)) + raise + + return container + @translate_exception def container_list(self, context): LOG.debug('Listing container...', context=context) @@ -344,3 +372,16 @@ class Manager(object): except Exception as e: LOG.exception(_LE("Unexpected exception: %s"), str(e)) raise e + + def _get_container_addresses(self, context, container): + LOG.debug('Showing container IP addresses...', context=context, + container=container) + try: + return self.driver.get_addresses(context, container) + except exception.DockerError as e: + LOG.error(_LE("Error occured while calling docker API: %s"), + six.text_type(e)) + raise + except Exception as e: + LOG.exception(_LE("Unexpected exception: %s"), str(e)) + raise e diff --git a/zun/conf/container_driver.py b/zun/conf/container_driver.py index 2b7035847..86814181a 100644 --- a/zun/conf/container_driver.py +++ b/zun/conf/container_driver.py @@ -28,7 +28,9 @@ Services which consume this: Interdependencies to other options: * None -""") +"""), + cfg.IntOpt('default_timeout', default=60 * 10, + help='Maximum time (in seconds) to wait for an event.'), ] diff --git a/zun/container/docker/driver.py b/zun/container/docker/driver.py index 050a588d0..b811bc7ec 100644 --- a/zun/container/docker/driver.py +++ b/zun/container/docker/driver.py @@ -11,17 +11,20 @@ # See the License for the specific language governing permissions and # limitations under the License. -from docker import errors import six +from docker import errors from oslo_log import log as logging +from zun.common.i18n import _LW from zun.common.utils import check_container_id +import zun.conf from zun.container.docker import utils as docker_utils from zun.container import driver from zun.objects import fields +CONF = zun.conf.CONF LOG = logging.getLogger(__name__) @@ -46,7 +49,7 @@ class DockerDriver(driver.ContainerDriver): response = docker.images(repo, quiet) return response - def create(self, container, image): + def create(self, container, sandbox_id, image): with docker_utils.docker_client() as docker: name = container.name if image['path']: @@ -67,14 +70,18 @@ class DockerDriver(driver.ContainerDriver): } host_config = {} - host_config['publish_all_ports'] = True + host_config['network_mode'] = 'container:%s' % sandbox_id + # TODO(hongbin): Uncomment this after docker-py add support for + # container mode for pid namespace. + # host_config['pid_mode'] = 'container:%s' % sandbox_id + host_config['ipc_mode'] = 'container:%s' % sandbox_id + host_config['volumes_from'] = sandbox_id if container.memory is not None: host_config['mem_limit'] = container.memory if container.cpu is not None: host_config['cpu_quota'] = int(100000 * container.cpu) host_config['cpu_period'] = 100000 - kwargs['host_config'] = \ - docker.create_host_config(**host_config) + kwargs['host_config'] = docker.create_host_config(**host_config) response = docker.create_container(image, **kwargs) container.container_id = response['Id'] @@ -196,3 +203,45 @@ class DockerDriver(driver.ContainerDriver): if six.PY2 and not isinstance(value, unicode): value = unicode(value) return value.encode('utf-8') + + def create_sandbox(self, context, container, image='kubernetes/pause'): + with docker_utils.docker_client() as docker: + name = self.get_sandbox_name(container) + response = docker.create_container(image, name=name) + sandbox_id = response['Id'] + docker.start(sandbox_id) + return sandbox_id + + def delete_sandbox(self, context, sandbox_id): + with docker_utils.docker_client() as docker: + docker.remove_container(sandbox_id, force=True) + + def get_sandbox_id(self, container): + if container.meta: + return container.meta.get('sandbox_id', None) + else: + LOG.warning(_LW("Unexpected missing of sandbox_id")) + return None + + def set_sandbox_id(self, container, id): + if container.meta is None: + container.meta = {'sandbox_id': id} + else: + container.meta['sandbox_id'] = id + + def get_sandbox_name(self, container): + return 'sandbox-' + container.uuid + + def get_addresses(self, context, container): + sandbox_id = self.get_sandbox_id(container) + with docker_utils.docker_client() as docker: + response = docker.inspect_container(sandbox_id) + addr = response["NetworkSettings"]["IPAddress"] + addresses = { + 'default': [ + { + 'addr': addr, + }, + ], + } + return addresses diff --git a/zun/container/driver.py b/zun/container/driver.py index bf7f1833f..cec5ce735 100644 --- a/zun/container/driver.py +++ b/zun/container/driver.py @@ -58,7 +58,7 @@ def load_container_driver(container_driver=None): class ContainerDriver(object): '''Base class for container drivers.''' - def create(self, container): + def create(self, container, sandbox_name=None): """Create a container.""" raise NotImplementedError() @@ -105,3 +105,27 @@ class ContainerDriver(object): def kill(self, container, signal): """kill signal to a container.""" raise NotImplementedError() + + def create_sandbox(self, context, container, **kwargs): + """Create a sandbox.""" + raise NotImplementedError() + + def delete_sandbox(self, context, id): + """Delete a sandbox.""" + raise NotImplementedError() + + def get_sandbox_id(self, container): + """Retrieve sandbox ID.""" + raise NotImplementedError() + + def set_sandbox_id(self, container, id): + """Set sandbox ID.""" + raise NotImplementedError() + + def get_sandbox_name(self, container): + """Retrieve sandbox name.""" + raise NotImplementedError() + + def get_addresses(self, context, container): + """Retrieve IP addresses of the container.""" + raise NotImplementedError() diff --git a/zun/db/sqlalchemy/alembic/versions/4a0c4f7a4a33_add_meta_addresses_to_container.py b/zun/db/sqlalchemy/alembic/versions/4a0c4f7a4a33_add_meta_addresses_to_container.py new file mode 100644 index 000000000..524d8d0db --- /dev/null +++ b/zun/db/sqlalchemy/alembic/versions/4a0c4f7a4a33_add_meta_addresses_to_container.py @@ -0,0 +1,39 @@ +# 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 meta addresses to container + +Revision ID: 4a0c4f7a4a33 +Revises: 43e1088c3389 +Create Date: 2016-11-20 12:18:44.086036 + +""" + +# revision identifiers, used by Alembic. +revision = '4a0c4f7a4a33' +down_revision = '43e1088c3389' +branch_labels = None +depends_on = None + +from alembic import op +import sqlalchemy as sa + +from zun.db.sqlalchemy import models + + +def upgrade(): + op.add_column('container', + sa.Column('meta', models.JSONEncodedDict(), + nullable=True)) + op.add_column('container', + sa.Column('addresses', models.JSONEncodedDict(), + nullable=True)) diff --git a/zun/db/sqlalchemy/models.py b/zun/db/sqlalchemy/models.py index 7794e4e3c..84535724a 100644 --- a/zun/db/sqlalchemy/models.py +++ b/zun/db/sqlalchemy/models.py @@ -142,6 +142,8 @@ class Container(Base): ports = Column(JSONEncodedList) hostname = Column(String(255)) labels = Column(JSONEncodedDict) + meta = Column(JSONEncodedDict) + addresses = Column(JSONEncodedDict) image_pull_policy = Column(Text, nullable=True) diff --git a/zun/objects/container.py b/zun/objects/container.py index 63e200833..b9a50ac1a 100644 --- a/zun/objects/container.py +++ b/zun/objects/container.py @@ -25,7 +25,9 @@ class Container(base.ZunPersistentObject, base.ZunObject, # Version 1.2: Add memory column # Version 1.3: Add task_state column # Version 1.4: Add cpu, workdir, ports, hostname and labels columns - VERSION = '1.4' + # Version 1.5: Add meta column + # Version 1.6: Add addresses column + VERSION = '1.6' fields = { 'id': fields.IntegerField(), @@ -46,7 +48,9 @@ class Container(base.ZunPersistentObject, base.ZunObject, 'ports': z_fields.ListOfIntegersField(nullable=True), 'hostname': fields.StringField(nullable=True), 'labels': fields.DictOfStringsField(nullable=True), - 'image_pull_policy': fields.StringField(nullable=True) + 'meta': fields.DictOfStringsField(nullable=True), + 'addresses': z_fields.JsonField(nullable=True), + 'image_pull_policy': fields.StringField(nullable=True), } @staticmethod diff --git a/zun/objects/fields.py b/zun/objects/fields.py index 8b49e957a..b5c653154 100644 --- a/zun/objects/fields.py +++ b/zun/objects/fields.py @@ -10,6 +10,9 @@ # License for the specific language governing permissions and limitations # under the License. +import six + +from oslo_serialization import jsonutils as json from oslo_versionedobjects import fields @@ -31,9 +34,9 @@ class ContainerStatusField(fields.BaseEnumField): class TaskState(fields.Enum): ALL = ( - IMAGE_PULLING, CONTAINER_CREATING, + IMAGE_PULLING, CONTAINER_CREATING, SANDBOX_CREATING, ) = ( - 'image_pulling', 'container_creating', + 'image_pulling', 'container_creating', 'sandbox_creating', ) def __init__(self): @@ -47,3 +50,21 @@ class TaskStateField(fields.BaseEnumField): class ListOfIntegersField(fields.AutoTypedField): AUTO_TYPE = fields.List(fields.Integer()) + + +class Json(fields.FieldType): + def coerce(self, obj, attr, value): + if isinstance(value, six.string_types): + loaded = json.loads(value) + return loaded + return value + + def from_primitive(self, obj, attr, value): + return self.coerce(obj, attr, value) + + def to_primitive(self, obj, attr, value): + return json.dumps(value) + + +class JsonField(fields.AutoTypedField): + AUTO_TYPE = Json() diff --git a/zun/tests/tempest/api/clients.py b/zun/tests/tempest/api/clients.py index e7166e2c8..eb26de679 100644 --- a/zun/tests/tempest/api/clients.py +++ b/zun/tests/tempest/api/clients.py @@ -18,6 +18,7 @@ from tempest import manager from zun.tests.tempest.api.models import container_model from zun.tests.tempest.api.models import service_model +from zun.tests.tempest import utils CONF = config.CONF @@ -105,3 +106,13 @@ class ZunClient(rest_client.RestClient): resp, body = self.get(self.services_uri(filters), **kwargs) return self.deserialize(resp, body, service_model.ServiceCollection) + + def ensure_container_created(self, container_id): + def container_created(): + _, container = self.get_container(container_id) + if container.status == 'Creating': + return False + else: + return True + + utils.wait_for_condition(container_created) diff --git a/zun/tests/tempest/api/test_containers.py b/zun/tests/tempest/api/test_containers.py index 20884730e..4929c8114 100644 --- a/zun/tests/tempest/api/test_containers.py +++ b/zun/tests/tempest/api/test_containers.py @@ -10,8 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -import time - from tempest.lib import decorators from zun.tests.tempest.api import clients @@ -43,6 +41,14 @@ class TestContainer(base.BaseZunTest): super(TestContainer, cls).resource_setup() + def tearDown(self): + _, model = self.container_client.list_containers() + for c in model.containers: + self.container_client.ensure_container_created(c['uuid']) + self.container_client.delete_container(c['uuid']) + + super(TestContainer, self).tearDown() + def _create_container(self, **kwargs): model = datagen.container_data(**kwargs) @@ -52,26 +58,6 @@ class TestContainer(base.BaseZunTest): self.container_client.delete_container(container_id, **kwargs) - def _wait_on_creation(self, container_id, timeout=60): - def _check_status(): - resp, model = self.container_client.get_container(container_id) - status = model.status - if status == 'Creating': - return False - else: - return True - time.sleep(1) - start_time = time.time() - end_time = time.time() + timeout - while time.time() < end_time: - result = _check_status() - if result: - return result - time.sleep(1) - raise Exception(("Timed out after %s seconds. Started " + - "on %s and ended on %s") % (timeout, start_time, - end_time)) - @decorators.idempotent_id('a04f61f2-15ae-4200-83b7-1f311b101f35') def test_container_create_list_delete(self): @@ -82,8 +68,7 @@ class TestContainer(base.BaseZunTest): self.assertEqual(200, resp.status) self.assertGreater(len(model.containers), 0) # NOTE(mkrai): Check and wait for container creation to get over - self._wait_on_creation(container.uuid) - + self.container_client.ensure_container_created(container.uuid) self._delete_container(container.uuid, headers={'force': True}) resp, model = self.container_client.list_containers() diff --git a/zun/tests/tempest/utils.py b/zun/tests/tempest/utils.py new file mode 100644 index 000000000..c61b92e1f --- /dev/null +++ b/zun/tests/tempest/utils.py @@ -0,0 +1,25 @@ +# 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 time + + +def wait_for_condition(condition, interval=1, timeout=60): + start_time = time.time() + end_time = time.time() + timeout + while time.time() < end_time: + result = condition() + if result: + return result + time.sleep(interval) + raise Exception(("Timed out after %s seconds. Started on %s and ended " + "on %s") % (timeout, start_time, end_time)) diff --git a/zun/tests/unit/compute/test_compute_manager.py b/zun/tests/unit/compute/test_compute_manager.py index 8bc9b8500..d35caeccf 100644 --- a/zun/tests/unit/compute/test_compute_manager.py +++ b/zun/tests/unit/compute/test_compute_manager.py @@ -62,56 +62,67 @@ class TestManager(base.TestCase): @mock.patch.object(Container, 'save') @mock.patch('zun.image.driver.pull_image') @mock.patch.object(fake_driver, 'create') - def test_container_create(self, mock_create, mock_pull, mock_save): + @mock.patch.object(fake_driver, 'create_sandbox') + def test_container_create(self, mock_create_sandbox, mock_create, + mock_pull, mock_save): container = Container(self.context, **utils.get_test_container()) mock_pull.return_value = 'fake_path' + mock_create_sandbox.return_value = 'fake_id' self.compute_manager._do_container_create(self.context, container) mock_save.assert_called_with() - mock_pull.assert_called_once_with(self.context, - container.image, - 'latest', 'always') - mock_create.assert_called_once_with(container, 'fake_path') + mock_pull.assert_any_call(self.context, container.image, 'latest', + 'always') + mock_create.assert_called_once_with(container, 'fake_id', 'fake_path') @mock.patch.object(Container, 'save') + @mock.patch.object(fake_driver, 'create_sandbox') @mock.patch('zun.image.driver.pull_image') @mock.patch.object(manager.Manager, '_fail_container') def test_container_create_pull_image_failed_docker_error( - self, mock_fail, mock_pull, mock_save): + self, mock_fail, mock_pull, mock_create_sandbox, mock_save): container = Container(self.context, **utils.get_test_container()) mock_pull.side_effect = exception.DockerError("Pull Failed") + mock_create_sandbox.return_value = mock.MagicMock() self.compute_manager._do_container_create(self.context, container) mock_fail.assert_called_once_with(container, "Pull Failed") @mock.patch.object(Container, 'save') + @mock.patch.object(fake_driver, 'create_sandbox') @mock.patch('zun.image.driver.pull_image') @mock.patch.object(manager.Manager, '_fail_container') def test_container_create_pull_image_failed_image_not_found( - self, mock_fail, mock_pull, mock_save): + self, mock_fail, mock_pull, mock_create_sandbox, mock_save): container = Container(self.context, **utils.get_test_container()) mock_pull.side_effect = exception.ImageNotFound("Image Not Found") + mock_create_sandbox.return_value = mock.MagicMock() self.compute_manager._do_container_create(self.context, container) mock_fail.assert_called_once_with(container, "Image Not Found") @mock.patch.object(Container, 'save') + @mock.patch.object(fake_driver, 'create_sandbox') @mock.patch('zun.image.driver.pull_image') @mock.patch.object(manager.Manager, '_fail_container') def test_container_create_pull_image_failed_zun_exception( - self, mock_fail, mock_pull, mock_save): + self, mock_fail, mock_pull, mock_create_sandbox, mock_save): container = Container(self.context, **utils.get_test_container()) mock_pull.side_effect = exception.ZunException( message="Image Not Found") + mock_create_sandbox.return_value = mock.MagicMock() self.compute_manager._do_container_create(self.context, container) mock_fail.assert_called_once_with(container, "Image Not Found") @mock.patch.object(Container, 'save') @mock.patch('zun.image.driver.pull_image') @mock.patch.object(fake_driver, 'create') + @mock.patch.object(fake_driver, 'create_sandbox') @mock.patch.object(manager.Manager, '_fail_container') def test_container_create_docker_create_failed(self, mock_fail, + mock_create_sandbox, mock_create, mock_pull, mock_save): container = Container(self.context, **utils.get_test_container()) mock_create.side_effect = exception.DockerError("Creation Failed") + mock_create_sandbox.return_value = mock.MagicMock() self.compute_manager._do_container_create(self.context, container) mock_fail.assert_called_once_with(container, "Creation Failed") @@ -127,10 +138,9 @@ class TestManager(base.TestCase): container.status = 'Stopped' self.compute_manager.container_run(self.context, container) mock_save.assert_called_with() - mock_pull.assert_called_once_with(self.context, - container.image, - 'latest', 'always') - mock_create.assert_called_once_with(container, 'fake_path') + mock_pull.assert_any_call(self.context, container.image, 'latest', + 'always') + mock_create.assert_called_once_with(container, None, 'fake_path') mock_start.assert_called_once_with(container) @mock.patch.object(Container, 'save') @@ -147,9 +157,8 @@ class TestManager(base.TestCase): container) mock_save.assert_called_with() mock_fail.assert_called_with(container, 'Image Not Found') - mock_pull.assert_called_once_with(self.context, - container.image, - 'latest', 'always') + mock_pull.assert_called_once_with(self.context, 'kubernetes/pause', + 'latest', 'ifnotpresent') @mock.patch.object(Container, 'save') @mock.patch('zun.image.driver.pull_image') @@ -165,9 +174,8 @@ class TestManager(base.TestCase): container) mock_save.assert_called_with() mock_fail.assert_called_with(container, 'Image Not Found') - mock_pull.assert_called_once_with(self.context, - container.image, - 'latest', 'always') + mock_pull.assert_called_once_with(self.context, 'kubernetes/pause', + 'latest', 'ifnotpresent') @mock.patch.object(Container, 'save') @mock.patch('zun.image.driver.pull_image') @@ -183,9 +191,8 @@ class TestManager(base.TestCase): container) mock_save.assert_called_with() mock_fail.assert_called_with(container, 'Docker Error occurred') - mock_pull.assert_called_once_with(self.context, - container.image, - 'latest', 'always') + mock_pull.assert_called_once_with(self.context, 'kubernetes/pause', + 'latest', 'ifnotpresent') @mock.patch.object(Container, 'save') @mock.patch('zun.image.driver.pull_image') @@ -204,10 +211,9 @@ class TestManager(base.TestCase): container) mock_save.assert_called_with() mock_fail.assert_called_with(container, 'Docker Error occurred') - mock_pull.assert_called_once_with(self.context, - container.image, - 'latest', 'always') - mock_create.assert_called_once_with(container, + mock_pull.assert_any_call(self.context, container.image, 'latest', + 'always') + mock_create.assert_called_once_with(container, None, {'name': 'nginx', 'path': None}) @mock.patch.object(manager.Manager, '_validate_container_state') diff --git a/zun/tests/unit/container/fake_driver.py b/zun/tests/unit/container/fake_driver.py index 37ec69f7c..f9f42dd6c 100644 --- a/zun/tests/unit/container/fake_driver.py +++ b/zun/tests/unit/container/fake_driver.py @@ -68,3 +68,18 @@ class FakeDriver(driver.ContainerDriver): @check_container_id def kill(self, container, signal=None): pass + + def create_sandbox(self, context, name, **kwargs): + pass + + def delete_sandbox(self, context, id): + pass + + def get_sandbox_id(self, container): + pass + + def set_sandbox_id(self, container, id): + pass + + def get_addresses(self, context, container): + pass diff --git a/zun/tests/unit/db/utils.py b/zun/tests/unit/db/utils.py index b2b4e11a5..25c3bbe31 100644 --- a/zun/tests/unit/db/utils.py +++ b/zun/tests/unit/db/utils.py @@ -44,6 +44,17 @@ def get_test_container(**kw): 'ports': kw.get('ports', [80, 443]), 'hostname': kw.get('hostname', 'testhost'), 'labels': kw.get('labels', {'key1': 'val1', 'key2': 'val2'}), + 'meta': kw.get('meta', {'key1': 'val1', 'key2': 'val2'}), + 'addresses': kw.get('addresses', { + 'private': [ + { + 'OS-EXT-IPS-MAC:mac_addr': 'fa:16:3e:04:da:76', + 'version': 4, + 'addr': '10.0.0.12', + 'OS-EXT-IPS:type': 'fixed' + }, + ], + }), 'image_pull_policy': kw.get('image_pull_policy', 'always'), }