From ddc2a81532f665c9f8abd89678ae76c70a09b0ab Mon Sep 17 00:00:00 2001 From: Hongbin Lu Date: Wed, 11 Jan 2017 10:21:32 -0600 Subject: [PATCH] Support multiple compute hosts This patch is for supporting deploying multiple instances of zun-compute to multiple hosts. In particular, if a container is created, it will be scheduled to a host picked by a scheduler. The host was recorded at the container object. Later, container life-cycle operations will call/cast to the host, to which the container was scheduled. The list of changes of this commit is as following: * Add a basic scheduler framework. The default scheduler is a basic scheduler that randomly choose a host. * In RPC, add support for sending message to specified host. * In compute, add APIs to schedule a container. * In context, add a method to elevate to admin privilege * In Nova driver, force the nova instance to be created in the scheduled host. This requires to elevate context before calling Nova APIs. * In objects and dbapi, add a method to list Zun services with specified binary. * In setup.cfg, add a scheduler entry point. * In cmd, use hostname as the rpc server ID (instead of a generated short ID). * In conf, use hostname as default value of CONF.host. Implements: blueprint support-multiple-hosts Implements: blueprint basic-container-scheduler Change-Id: I6955881e3087c488eb9cd857cbbd19f49f6318fc --- setup.cfg | 4 + zun/api/controllers/v1/containers.py | 107 +++++++++-------- zun/api/controllers/v1/images.py | 8 +- zun/api/hooks.py | 6 +- zun/cmd/compute.py | 4 +- zun/common/context.py | 14 +++ zun/common/exception.py | 4 + zun/common/rpc_service.py | 10 +- zun/compute/api.py | 94 +++++++++++++++ zun/compute/rpcapi.py | 48 +++++--- zun/conf/__init__.py | 2 + zun/conf/scheduler.py | 49 ++++++++ zun/conf/services.py | 3 +- zun/container/docker/driver.py | 26 ++++- zun/db/api.py | 14 ++- zun/db/sqlalchemy/api.py | 5 + zun/objects/zun_service.py | 6 + zun/scheduler/__init__.py | 0 zun/scheduler/chance_scheduler.py | 48 ++++++++ zun/scheduler/client.py | 33 ++++++ zun/scheduler/driver.py | 55 +++++++++ .../api/controllers/v1/test_containers.py | 108 +++++++++--------- .../unit/api/controllers/v1/test_images.py | 28 ++--- zun/tests/unit/common/test_context.py | 10 ++ .../container/docker/test_docker_driver.py | 7 +- zun/tests/unit/db/test_zun_service.py | 99 ++++++++++++++++ zun/tests/unit/objects/test_zun_service.py | 10 ++ zun/tests/unit/scheduler/__init__.py | 0 zun/tests/unit/scheduler/fake_scheduler.py | 19 +++ .../unit/scheduler/test_chance_scheduler.py | 61 ++++++++++ zun/tests/unit/scheduler/test_client.py | 47 ++++++++ zun/tests/unit/scheduler/test_scheduler.py | 48 ++++++++ 32 files changed, 821 insertions(+), 156 deletions(-) create mode 100644 zun/compute/api.py create mode 100644 zun/conf/scheduler.py create mode 100644 zun/scheduler/__init__.py create mode 100644 zun/scheduler/chance_scheduler.py create mode 100644 zun/scheduler/client.py create mode 100644 zun/scheduler/driver.py create mode 100644 zun/tests/unit/scheduler/__init__.py create mode 100644 zun/tests/unit/scheduler/fake_scheduler.py create mode 100644 zun/tests/unit/scheduler/test_chance_scheduler.py create mode 100644 zun/tests/unit/scheduler/test_client.py create mode 100644 zun/tests/unit/scheduler/test_scheduler.py diff --git a/setup.cfg b/setup.cfg index def19cb4e..d2cd2bc3c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -60,5 +60,9 @@ oslo.config.opts.defaults = zun.database.migration_backend = sqlalchemy = zun.db.sqlalchemy.migration +zun.scheduler.driver = + chance_scheduler = zun.scheduler.chance_scheduler:ChanceScheduler + fake_scheduler = zun.tests.unit.scheduler.fake_scheduler:FakeScheduler + tempest.test_plugins = zun_tests = zun.tests.tempest.plugin:ZunTempestPlugin diff --git a/zun/api/controllers/v1/containers.py b/zun/api/controllers/v1/containers.py index 515c6a4cb..16e42486a 100644 --- a/zun/api/controllers/v1/containers.py +++ b/zun/api/controllers/v1/containers.py @@ -78,7 +78,7 @@ class Container(base.APIBase): 'labels', 'addresses', 'image_pull_policy', - 'host' + 'host', } def __init__(self, **kwargs): @@ -196,6 +196,7 @@ class ContainersController(rest.RestController): def _get_containers_collection(self, **kwargs): context = pecan.request.context + compute_api = pecan.request.compute_api limit = api_utils.validate_limit(kwargs.get('limit')) sort_dir = api_utils.validate_sort_dir(kwargs.get('sort_dir', 'asc')) sort_key = kwargs.get('sort_key', 'id') @@ -217,7 +218,7 @@ class ContainersController(rest.RestController): for i, c in enumerate(containers): try: - containers[i] = pecan.request.rpcapi.container_show(context, c) + containers[i] = compute_api.container_show(context, c) except Exception as e: LOG.exception(_LE("Error while list container %(uuid)s: " "%(e)s."), @@ -240,7 +241,8 @@ class ContainersController(rest.RestController): container = _get_container(container_id) check_policy_on_container(container.as_dict(), "container:get") context = pecan.request.context - container = pecan.request.rpcapi.container_show(context, container) + compute_api = pecan.request.compute_api + container = compute_api.container_show(context, container) return Container.convert_with_links(container.as_dict()) def _generate_name_for_container(self): @@ -254,43 +256,43 @@ class ContainersController(rest.RestController): @exception.wrap_pecan_controller_exception @validation.validated(schema.container_create) def post(self, run=False, **container_dict): - """Create a new container. + """Create a new container. - :param run: if true, starts the container - :param container: a container within the request body. - """ - context = pecan.request.context - policy.enforce(context, "container:create", - action="container:create") - # NOTE(mkrai): Intent here is to check the existence of image - # before proceeding to create container. If image is not found, - # container create will fail with 400 status. - images = pecan.request.rpcapi.image_search(context, - container_dict['image'], - exact_match=True) - if not images: - raise exception.ImageNotFound(container_dict['image']) - container_dict['project_id'] = context.project_id - container_dict['user_id'] = context.user_id - name = container_dict.get('name') or \ - self._generate_name_for_container() - container_dict['name'] = name - if container_dict.get('memory'): - container_dict['memory'] = \ - str(container_dict['memory']) + 'M' - container_dict['status'] = fields.ContainerStatus.CREATING - new_container = objects.Container(context, **container_dict) - new_container.create(context) + :param run: if true, starts the container + :param container: a container within the request body. + """ + context = pecan.request.context + compute_api = pecan.request.compute_api + policy.enforce(context, "container:create", + action="container:create") + # NOTE(mkrai): Intent here is to check the existence of image + # before proceeding to create container. If image is not found, + # container create will fail with 400 status. + images = compute_api.image_search(context, container_dict['image'], + True) + if not images: + raise exception.ImageNotFound(container_dict['image']) + container_dict['project_id'] = context.project_id + container_dict['user_id'] = context.user_id + name = container_dict.get('name') or \ + self._generate_name_for_container() + container_dict['name'] = name + if container_dict.get('memory'): + container_dict['memory'] = \ + str(container_dict['memory']) + 'M' + container_dict['status'] = fields.ContainerStatus.CREATING + new_container = objects.Container(context, **container_dict) + new_container.create(context) - if run: - pecan.request.rpcapi.container_run(context, new_container) - else: - pecan.request.rpcapi.container_create(context, new_container) - # Set the HTTP Location Header - pecan.response.location = link.build_url('containers', - new_container.uuid) - pecan.response.status = 202 - return Container.convert_with_links(new_container.as_dict()) + if run: + compute_api.container_run(context, new_container) + else: + compute_api.container_create(context, new_container) + # Set the HTTP Location Header + pecan.response.location = link.build_url('containers', + new_container.uuid) + pecan.response.status = 202 + return Container.convert_with_links(new_container.as_dict()) @pecan.expose('json') @exception.wrap_pecan_controller_exception @@ -336,7 +338,8 @@ class ContainersController(rest.RestController): if not force: utils.validate_container_state(container, 'delete') context = pecan.request.context - pecan.request.rpcapi.container_delete(context, container, force) + compute_api = pecan.request.compute_api + compute_api.container_delete(context, container, force) container.destroy(context) pecan.response.status = 204 @@ -349,7 +352,8 @@ class ContainersController(rest.RestController): LOG.debug('Calling compute.container_start with %s', container.uuid) context = pecan.request.context - pecan.request.rpcapi.container_start(context, container) + compute_api = pecan.request.compute_api + compute_api.container_start(context, container) pecan.response.status = 202 @pecan.expose('json') @@ -361,7 +365,8 @@ class ContainersController(rest.RestController): LOG.debug('Calling compute.container_stop with %s' % container.uuid) context = pecan.request.context - pecan.request.rpcapi.container_stop(context, container, timeout) + compute_api = pecan.request.compute_api + compute_api.container_stop(context, container, timeout) pecan.response.status = 202 @pecan.expose('json') @@ -373,7 +378,8 @@ class ContainersController(rest.RestController): LOG.debug('Calling compute.container_reboot with %s' % container.uuid) context = pecan.request.context - pecan.request.rpcapi.container_reboot(context, container, timeout) + compute_api = pecan.request.compute_api + compute_api.container_reboot(context, container, timeout) pecan.response.status = 202 @pecan.expose('json') @@ -385,7 +391,8 @@ class ContainersController(rest.RestController): LOG.debug('Calling compute.container_pause with %s' % container.uuid) context = pecan.request.context - pecan.request.rpcapi.container_pause(context, container) + compute_api = pecan.request.compute_api + compute_api.container_pause(context, container) pecan.response.status = 202 @pecan.expose('json') @@ -397,7 +404,8 @@ class ContainersController(rest.RestController): LOG.debug('Calling compute.container_unpause with %s' % container.uuid) context = pecan.request.context - pecan.request.rpcapi.container_unpause(context, container) + compute_api = pecan.request.compute_api + compute_api.container_unpause(context, container) pecan.response.status = 202 @pecan.expose('json') @@ -408,7 +416,8 @@ class ContainersController(rest.RestController): LOG.debug('Calling compute.container_logs with %s' % container.uuid) context = pecan.request.context - return pecan.request.rpcapi.container_logs(context, container) + compute_api = pecan.request.compute_api + return compute_api.container_logs(context, container) @pecan.expose('json') @exception.wrap_pecan_controller_exception @@ -419,8 +428,8 @@ class ContainersController(rest.RestController): LOG.debug('Calling compute.container_exec with %s command %s' % (container.uuid, kw['command'])) context = pecan.request.context - return pecan.request.rpcapi.container_exec(context, container, - kw['command']) + compute_api = pecan.request.compute_api + return compute_api.container_exec(context, container, kw['command']) @pecan.expose('json') @exception.wrap_pecan_controller_exception @@ -431,6 +440,6 @@ class ContainersController(rest.RestController): LOG.debug('Calling compute.container_kill with %s signal %s' % (container.uuid, kw.get('signal', kw.get('signal')))) context = pecan.request.context - pecan.request.rpcapi.container_kill(context, container, - kw.get('signal', None)) + compute_api = pecan.request.compute_api + compute_api.container_kill(context, container, kw.get('signal')) pecan.response.status = 202 diff --git a/zun/api/controllers/v1/images.py b/zun/api/controllers/v1/images.py index ca26b807a..5bd4c2e3a 100644 --- a/zun/api/controllers/v1/images.py +++ b/zun/api/controllers/v1/images.py @@ -171,7 +171,7 @@ class ImagesController(rest.RestController): filters=filters) for i, c in enumerate(images): try: - images[i] = pecan.request.rpcapi.image_show(context, c) + images[i] = pecan.request.compute_api.image_show(context, c) except Exception as e: LOG.exception(_LE("Error while list image %(uuid)s: " "%(e)s."), {'uuid': c.uuid, 'e': e}) @@ -201,7 +201,7 @@ class ImagesController(rest.RestController): repo_tag) new_image = objects.Image(context, **image_dict) new_image.pull(context) - pecan.request.rpcapi.image_pull(context, new_image) + pecan.request.compute_api.image_pull(context, new_image) # Set the HTTP Location Header pecan.response.location = link.build_url('images', new_image.uuid) pecan.response.status = 202 @@ -217,5 +217,5 @@ class ImagesController(rest.RestController): action="image:search") LOG.debug('Calling compute.image_search with %s' % image) - return pecan.request.rpcapi.image_search(context, image, - exact_match=exact_match) + return pecan.request.compute_api.image_search(context, image, + exact_match) diff --git a/zun/api/hooks.py b/zun/api/hooks.py index 47b058120..742534190 100644 --- a/zun/api/hooks.py +++ b/zun/api/hooks.py @@ -17,7 +17,7 @@ from oslo_config import cfg from pecan import hooks from zun.common import context -from zun.compute import rpcapi as compute_rpcapi +from zun.compute import api as compute_api import zun.conf CONF = zun.conf.CONF @@ -80,8 +80,8 @@ class RPCHook(hooks.PecanHook): """Attach the rpcapi object to the request so controllers can get to it.""" def before(self, state): - state.request.rpcapi = compute_rpcapi.API( - context=state.request.context) + context = state.request.context + state.request.compute_api = compute_api.API(context) class NoExceptionTracebackHook(hooks.PecanHook): diff --git a/zun/cmd/compute.py b/zun/cmd/compute.py index b7cb74c0e..609e62431 100644 --- a/zun/cmd/compute.py +++ b/zun/cmd/compute.py @@ -21,7 +21,6 @@ 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.compute import manager as compute_manager import zun.conf @@ -37,12 +36,11 @@ def main(): CONF.import_opt('topic', 'zun.conf.compute', group='compute') - compute_id = short_id.generate_id() endpoints = [ compute_manager.Manager(), ] - server = rpc_service.Service.create(CONF.compute.topic, compute_id, + server = rpc_service.Service.create(CONF.compute.topic, CONF.host, endpoints, binary='zun-compute') launcher = service.launch(CONF, server) launcher.wait() diff --git a/zun/common/context.py b/zun/common/context.py index ba1356e9c..55cbda64f 100644 --- a/zun/common/context.py +++ b/zun/common/context.py @@ -10,6 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. +import copy from eventlet.green import threading from oslo_context import context @@ -84,6 +85,19 @@ class RequestContext(context.RequestContext): def from_dict(cls, values): return cls(**values) + def elevated(self): + """Return a version of this context with admin flag set.""" + context = copy.copy(self) + # context.roles must be deepcopied to leave original roles + # without changes + context.roles = copy.deepcopy(self.roles) + context.is_admin = True + + if 'admin' not in context.roles: + context.roles.append('admin') + + return context + def make_context(*args, **kwargs): return RequestContext(*args, **kwargs) diff --git a/zun/common/exception.py b/zun/common/exception.py index 76e5d7e79..5981c0062 100644 --- a/zun/common/exception.py +++ b/zun/common/exception.py @@ -386,3 +386,7 @@ class ServerUnknownStatus(ZunException): class EntityNotFound(ZunException): message = _("The %(entity)s (%(name)s) could not be found.") + + +class NoValidHost(ZunException): + message = _("No valid host was found. %(reason)s") diff --git a/zun/common/rpc_service.py b/zun/common/rpc_service.py index ff96683a4..6a93dd036 100644 --- a/zun/common/rpc_service.py +++ b/zun/common/rpc_service.py @@ -79,11 +79,13 @@ class API(object): serializer=serializer, timeout=timeout) - def _call(self, method, *args, **kwargs): - return self._client.call(self._context, method, *args, **kwargs) + def _call(self, server, method, *args, **kwargs): + cctxt = self._client.prepare(server=server) + return cctxt.call(self._context, method, *args, **kwargs) - def _cast(self, method, *args, **kwargs): - self._client.cast(self._context, method, *args, **kwargs) + def _cast(self, server, method, *args, **kwargs): + cctxt = self._client.prepare(server=server) + return cctxt.cast(self._context, method, *args, **kwargs) def echo(self, message): self._cast('echo', message=message) diff --git a/zun/compute/api.py b/zun/compute/api.py new file mode 100644 index 000000000..5bd385a18 --- /dev/null +++ b/zun/compute/api.py @@ -0,0 +1,94 @@ +# 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. + +"""Handles all requests relating to compute resources (e.g. containers, +networking and storage of containers, and compute hosts on which they run).""" + +from zun.compute import rpcapi +from zun.objects import fields +from zun.scheduler import client as scheduler_client + + +class API(object): + """API for interacting with the compute manager.""" + + def __init__(self, context): + self.rpcapi = rpcapi.API(context=context) + self.scheduler_client = scheduler_client.SchedulerClient() + super(API, self).__init__() + + def container_create(self, context, new_container): + try: + self._schedule_container(context, new_container) + except Exception as exc: + new_container.status = fields.ContainerStatus.ERROR + new_container.status_reason = str(exc) + new_container.save() + return + + self.rpcapi.container_create(context, new_container) + + def container_run(self, context, new_container): + try: + self._schedule_container(context, new_container) + except Exception as exc: + new_container.status = fields.ContainerStatus.ERROR + new_container.status_reason = str(exc) + new_container.save() + return + + self.rpcapi.container_run(context, new_container) + + def _schedule_container(self, context, new_container): + dests = self.scheduler_client.select_destinations(context, + [new_container]) + new_container.host = dests[0]['host'] + new_container.save() + + def container_delete(self, context, container, *args): + return self.rpcapi.container_delete(context, container, *args) + + def container_show(self, context, container, *args): + return self.rpcapi.container_show(context, container, *args) + + def container_reboot(self, context, container, *args): + return self.rpcapi.container_reboot(context, container, *args) + + def container_stop(self, context, container, *args): + return self.rpcapi.container_stop(context, container, *args) + + def container_start(self, context, container, *args): + return self.rpcapi.container_start(context, container, *args) + + def container_pause(self, context, container, *args): + return self.rpcapi.container_pause(context, container, *args) + + def container_unpause(self, context, container, *args): + return self.rpcapi.container_unpause(context, container, *args) + + def container_logs(self, context, container, *args): + return self.rpcapi.container_logs(context, container, *args) + + def container_exec(self, context, container, *args): + return self.rpcapi.container_exec(context, container, *args) + + def container_kill(self, context, container, *args): + return self.rpcapi.container_kill(context, container, *args) + + def image_show(self, context, image, *args): + return self.rpcapi.image_show(context, image, *args) + + def image_pull(self, context, image, *args): + return self.rpcapi.image_pull(context, image, *args) + + def image_search(self, context, image, *args): + return self.rpcapi.image_search(context, image, *args) diff --git a/zun/compute/rpcapi.py b/zun/compute/rpcapi.py index d1a02d417..5ec964d8e 100644 --- a/zun/compute/rpcapi.py +++ b/zun/compute/rpcapi.py @@ -35,51 +35,67 @@ class API(rpc_service.API): transport, context, topic=zun.conf.CONF.compute.topic) def container_create(self, context, container): - self._cast('container_create', container=container) + self._cast(container.host, 'container_create', container=container) def container_run(self, context, container): - self._cast('container_run', container=container) + self._cast(container.host, 'container_run', container=container) def container_delete(self, context, container, force): - return self._call('container_delete', container=container, force=force) + return self._call(container.host, 'container_delete', + container=container, force=force) def container_show(self, context, container): - return self._call('container_show', container=container) + return self._call(container.host, 'container_show', + container=container) def container_reboot(self, context, container, timeout): - self._cast('container_reboot', container=container, + self._cast(container.host, 'container_reboot', container=container, timeout=timeout) def container_stop(self, context, container, timeout): - self._cast('container_stop', container=container, + self._cast(container.host, 'container_stop', container=container, timeout=timeout) def container_start(self, context, container): - self._cast('container_start', container=container) + host = container.host + self._cast(host, 'container_start', container=container) def container_pause(self, context, container): - self._cast('container_pause', container=container) + self._cast(container.host, 'container_pause', container=container) def container_unpause(self, context, container): - self._cast('container_unpause', container=container) + self._cast(container.host, 'container_unpause', container=container) def container_logs(self, context, container): - return self._call('container_logs', container=container) + host = container.host + return self._call(host, 'container_logs', container=container) def container_exec(self, context, container, command): - return self._call('container_exec', container=container, - command=command) + return self._call(container.host, 'container_exec', + container=container, command=command) def container_kill(self, context, container, signal): - self._cast('container_kill', container=container, + self._cast(container.host, 'container_kill', container=container, signal=signal) def image_show(self, context, image): - return self._call('image_show', image=image) + # NOTE(hongbin): Image API doesn't support multiple compute nodes + # scenario yet, so we temporarily set host to None and rpc will + # choose an arbitrary host. + host = None + return self._call(host, 'image_show', image=image) def image_pull(self, context, image): - self._cast('image_pull', image=image) + # NOTE(hongbin): Image API doesn't support multiple compute nodes + # scenario yet, so we temporarily set host to None and rpc will + # choose an arbitrary host. + host = None + self._cast(host, 'image_pull', image=image) def image_search(self, context, image, exact_match): - return self._call('image_search', image=image, + # NOTE(hongbin): Image API doesn't support multiple compute nodes + # scenario yet, so we temporarily set host to None and rpc will + # choose an arbitrary host. + host = None + return self._call(host, 'image_search', image=image, exact_match=exact_match) diff --git a/zun/conf/__init__.py b/zun/conf/__init__.py index cb4dd3f25..2b0c3c671 100644 --- a/zun/conf/__init__.py +++ b/zun/conf/__init__.py @@ -23,6 +23,7 @@ from zun.conf import glance_client from zun.conf import image_driver from zun.conf import nova_client from zun.conf import path +from zun.conf import scheduler from zun.conf import services from zun.conf import zun_client @@ -37,5 +38,6 @@ glance_client.register_opts(CONF) image_driver.register_opts(CONF) nova_client.register_opts(CONF) path.register_opts(CONF) +scheduler.register_opts(CONF) services.register_opts(CONF) zun_client.register_opts(CONF) diff --git a/zun/conf/scheduler.py b/zun/conf/scheduler.py new file mode 100644 index 000000000..35e3132ce --- /dev/null +++ b/zun/conf/scheduler.py @@ -0,0 +1,49 @@ +# Copyright 2015 OpenStack Foundation +# 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. + +from oslo_config import cfg + + +scheduler_group = cfg.OptGroup(name="scheduler", + title="Scheduler configuration") + +scheduler_opts = [ + cfg.StrOpt("driver", + default="chance_scheduler", + choices=("chance_scheduler", "fake_scheduler"), + help=""" +The class of the driver used by the scheduler. + +The options are chosen from the entry points under the namespace +'zun.scheduler.driver' in 'setup.cfg'. + +Possible values: + +* A string, where the string corresponds to the class name of a scheduler + driver. There are a number of options available: +** 'chance_scheduler', which simply picks a host at random +** A custom scheduler driver. In this case, you will be responsible for + creating and maintaining the entry point in your 'setup.cfg' file +"""), +] + + +def register_opts(conf): + conf.register_group(scheduler_group) + conf.register_opts(scheduler_opts, group=scheduler_group) + + +def list_opts(): + return {scheduler_group: scheduler_opts} diff --git a/zun/conf/services.py b/zun/conf/services.py index f7552b813..63922ba15 100644 --- a/zun/conf/services.py +++ b/zun/conf/services.py @@ -22,7 +22,8 @@ from zun.common.i18n import _ service_opts = [ cfg.StrOpt('host', - default=socket.getfqdn(), + default=socket.gethostname(), + sample_default='localhost', help=_('Name of this node. This can be an opaque identifier. ' 'It is not necessarily a hostname, FQDN, or IP address. ' 'However, the node name must be valid within ' diff --git a/zun/container/docker/driver.py b/zun/container/docker/driver.py index 74260f741..c5af14858 100644 --- a/zun/container/docker/driver.py +++ b/zun/container/docker/driver.py @@ -290,11 +290,24 @@ class NovaDockerDriver(DockerDriver): def create_sandbox(self, context, container, key_name=None, flavor='m1.small', image='kubernetes/pause', nics='auto'): + # FIXME(hongbin): We elevate to admin privilege because the default + # policy in nova disallows non-admin users to create instance in + # specified host. This is not ideal because all nova instances will + # be created at service admin tenant now, which breaks the + # multi-tenancy model. We need to fix it. + elevated = context.elevated() + novaclient = nova.NovaClient(elevated) name = self.get_sandbox_name(container) - novaclient = nova.NovaClient(context) + if container.host != CONF.host: + raise exception.ZunException(_( + "Host mismatch: container should be created at host '%s'.") % + container.host) + # NOTE(hongbin): The format of availability zone is ZONE:HOST:NODE + # However, we just want to specify host, so it is ':HOST:' + az = ':%s:' % container.host sandbox = novaclient.create_server(name=name, image=image, flavor=flavor, key_name=key_name, - nics=nics) + nics=nics, availability_zone=az) self._ensure_active(novaclient, sandbox) sandbox_id = self._find_container_by_server_name(name) return sandbox_id @@ -313,7 +326,8 @@ class NovaDockerDriver(DockerDriver): success_msg=success_msg, timeout_msg=timeout_msg) def delete_sandbox(self, context, sandbox_id): - novaclient = nova.NovaClient(context) + elevated = context.elevated() + novaclient = nova.NovaClient(elevated) server_name = self._find_server_by_container_id(sandbox_id) if not server_name: LOG.warning(_LW("Cannot find server name for sandbox %s") % @@ -324,7 +338,8 @@ class NovaDockerDriver(DockerDriver): self._ensure_deleted(novaclient, server_id) def stop_sandbox(self, context, sandbox_id): - novaclient = nova.NovaClient(context) + elevated = context.elevated() + novaclient = nova.NovaClient(elevated) server_name = self._find_server_by_container_id(sandbox_id) if not server_name: LOG.warning(_LW("Cannot find server name for sandbox %s") % @@ -346,7 +361,8 @@ class NovaDockerDriver(DockerDriver): success_msg=success_msg, timeout_msg=timeout_msg) def get_addresses(self, context, container): - novaclient = nova.NovaClient(context) + elevated = context.elevated() + novaclient = nova.NovaClient(elevated) sandbox_id = self.get_sandbox_id(container) if sandbox_id: server_name = self._find_server_by_container_id(sandbox_id) diff --git a/zun/db/api.py b/zun/db/api.py index 24d5a13ae..105ac7697 100644 --- a/zun/db/api.py +++ b/zun/db/api.py @@ -58,7 +58,7 @@ class Connection(object): def list_container(cls, context, filters=None, limit=None, marker=None, sort_key=None, sort_dir=None): - """Get matching containers. + """List matching containers. Return a list of the specified columns for all containers that match the specified filters. @@ -219,6 +219,18 @@ class Connection(object): return dbdriver.get_zun_service_list(disabled, limit, marker, sort_key, sort_dir) + @classmethod + def list_zun_service_by_binary(cls, context, binary): + """List matching zun services. + + Return a list of the specified binary. + :param context: The security context + :param binary: The name of the binary. + :returns: A list of tuples of the specified binary. + """ + dbdriver = get_instance() + return dbdriver.list_zun_service_by_binary(binary) + @classmethod def pull_image(cls, context, values): """Create a new image. diff --git a/zun/db/sqlalchemy/api.py b/zun/db/sqlalchemy/api.py index d4fe1012b..bad744d74 100644 --- a/zun/db/sqlalchemy/api.py +++ b/zun/db/sqlalchemy/api.py @@ -293,6 +293,11 @@ class Connection(api.Connection): return _paginate_query(models.ZunService, limit, marker, sort_key, sort_dir, query) + def list_zun_service_by_binary(cls, binary): + query = model_query(models.ZunService) + query = query.filter_by(binary=binary) + return _paginate_query(models.ZunService, query=query) + def pull_image(self, context, values): # ensure defaults are present for new containers if not values.get('uuid'): diff --git a/zun/objects/zun_service.py b/zun/objects/zun_service.py index 7f24324bf..b8eda92e3 100644 --- a/zun/objects/zun_service.py +++ b/zun/objects/zun_service.py @@ -84,6 +84,12 @@ class ZunService(base.ZunPersistentObject, base.ZunObject): return ZunService._from_db_object_list(db_zun_services, cls, context) + @base.remotable_classmethod + def list_by_binary(cls, context, binary): + db_zun_services = dbapi.Connection.list_zun_service_by_binary( + context, binary) + return ZunService._from_db_object_list(db_zun_services, cls, context) + @base.remotable def create(self, context=None): """Create a ZunService record in the DB. diff --git a/zun/scheduler/__init__.py b/zun/scheduler/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/zun/scheduler/chance_scheduler.py b/zun/scheduler/chance_scheduler.py new file mode 100644 index 000000000..3fbc7ee68 --- /dev/null +++ b/zun/scheduler/chance_scheduler.py @@ -0,0 +1,48 @@ +# 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. + +""" +Chance (Random) Scheduler implementation +""" + +import random + +from zun.common import exception +from zun.common.i18n import _ +from zun.scheduler import driver + + +class ChanceScheduler(driver.Scheduler): + """Implements Scheduler as a random node selector.""" + + def _schedule(self, context, container): + """Picks a host that is up at random.""" + hosts = self.hosts_up(context) + if not hosts: + msg = _("Is the appropriate service running?") + raise exception.NoValidHost(reason=msg) + + return random.choice(hosts) + + def select_destinations(self, context, containers): + """Selects random destinations.""" + dests = [] + for container in containers: + host = self._schedule(context, container) + host_state = dict(host=host, nodename=None, limits=None) + dests.append(host_state) + + if len(dests) < 1: + reason = _('There are not enough hosts available.') + raise exception.NoValidHost(reason=reason) + + return dests diff --git a/zun/scheduler/client.py b/zun/scheduler/client.py new file mode 100644 index 000000000..aa5a22f2c --- /dev/null +++ b/zun/scheduler/client.py @@ -0,0 +1,33 @@ +# Copyright (c) 2014 Red Hat, Inc. +# 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. + +from stevedore import driver +import zun.conf + +CONF = zun.conf.CONF + + +class SchedulerClient(object): + """Client library for placing calls to the scheduler.""" + + def __init__(self): + scheduler_driver = CONF.scheduler.driver + self.driver = driver.DriverManager( + "zun.scheduler.driver", + scheduler_driver, + invoke_on_load=True).driver + + def select_destinations(self, context, containers): + return self.driver.select_destinations(context, containers) diff --git a/zun/scheduler/driver.py b/zun/scheduler/driver.py new file mode 100644 index 000000000..a7a3e5645 --- /dev/null +++ b/zun/scheduler/driver.py @@ -0,0 +1,55 @@ +# Copyright (c) 2010 OpenStack Foundation +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# 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. + +""" +Scheduler base class that all Schedulers should inherit from +""" + +import abc + +import six + +from zun.api import servicegroup +import zun.conf +from zun import objects + +CONF = zun.conf.CONF + + +@six.add_metaclass(abc.ABCMeta) +class Scheduler(object): + """The base class that all Scheduler classes should inherit from.""" + + def __init__(self): + self.servicegroup_api = servicegroup.ServiceGroup() + + def hosts_up(self, context): + """Return the list of hosts that have a running service.""" + + services = objects.ZunService.list_by_binary(context, 'zun-compute') + return [service.host + for service in services + if self.servicegroup_api.service_is_up(service)] + + @abc.abstractmethod + def select_destinations(self, context, containers): + """Must override select_destinations method. + + :return: A list of dicts with 'host', 'nodename' and 'limits' as keys + that satisfies the request_spec and filter_properties. + """ + return [] diff --git a/zun/tests/unit/api/controllers/v1/test_containers.py b/zun/tests/unit/api/controllers/v1/test_containers.py index ce83ecde2..982a3c9c9 100644 --- a/zun/tests/unit/api/controllers/v1/test_containers.py +++ b/zun/tests/unit/api/controllers/v1/test_containers.py @@ -25,8 +25,8 @@ from zun.tests.unit.objects import utils as obj_utils class TestContainerController(api_base.FunctionalTest): - @patch('zun.compute.rpcapi.API.container_run') - @patch('zun.compute.rpcapi.API.image_search') + @patch('zun.compute.api.API.container_run') + @patch('zun.compute.api.API.image_search') def test_run_container(self, mock_search, mock_container_run): mock_container_run.side_effect = lambda x, y: y @@ -40,8 +40,8 @@ class TestContainerController(api_base.FunctionalTest): self.assertEqual(202, response.status_int) self.assertTrue(mock_container_run.called) - @patch('zun.compute.rpcapi.API.container_create') - @patch('zun.compute.rpcapi.API.image_search') + @patch('zun.compute.api.API.container_create') + @patch('zun.compute.api.API.image_search') def test_create_container(self, mock_search, mock_container_create): mock_container_create.side_effect = lambda x, y: y @@ -55,7 +55,7 @@ class TestContainerController(api_base.FunctionalTest): self.assertEqual(202, response.status_int) self.assertTrue(mock_container_create.called) - @patch('zun.compute.rpcapi.API.container_create') + @patch('zun.compute.api.API.container_create') def test_create_container_image_not_specified(self, mock_container_create): params = ('{"name": "MyDocker",' @@ -68,8 +68,8 @@ class TestContainerController(api_base.FunctionalTest): content_type='application/json') self.assertTrue(mock_container_create.not_called) - @patch('zun.compute.rpcapi.API.container_create') - @patch('zun.compute.rpcapi.API.image_search') + @patch('zun.compute.api.API.container_create') + @patch('zun.compute.api.API.image_search') def test_create_container_image_not_found(self, mock_search, mock_container_create): mock_container_create.side_effect = lambda x, y: y @@ -81,8 +81,8 @@ class TestContainerController(api_base.FunctionalTest): self.assertEqual(404, response.status_int) self.assertFalse(mock_container_create.called) - @patch('zun.compute.rpcapi.API.container_create') - @patch('zun.compute.rpcapi.API.image_search') + @patch('zun.compute.api.API.container_create') + @patch('zun.compute.api.API.image_search') def test_create_container_set_project_id_and_user_id( self, mock_search, mock_container_create): def _create_side_effect(cnxt, container): @@ -98,8 +98,8 @@ class TestContainerController(api_base.FunctionalTest): params=params, content_type='application/json') - @patch('zun.compute.rpcapi.API.container_create') - @patch('zun.compute.rpcapi.API.image_search') + @patch('zun.compute.api.API.container_create') + @patch('zun.compute.api.API.image_search') def test_create_container_resp_has_status_reason(self, mock_search, mock_container_create): mock_container_create.side_effect = lambda x, y: y @@ -113,10 +113,10 @@ class TestContainerController(api_base.FunctionalTest): self.assertEqual(202, response.status_int) self.assertIn('status_reason', response.json.keys()) - @patch('zun.compute.rpcapi.API.container_show') - @patch('zun.compute.rpcapi.API.container_create') - @patch('zun.compute.rpcapi.API.container_delete') - @patch('zun.compute.rpcapi.API.image_search') + @patch('zun.compute.api.API.container_show') + @patch('zun.compute.api.API.container_create') + @patch('zun.compute.api.API.container_delete') + @patch('zun.compute.api.API.image_search') def test_create_container_with_command(self, mock_search, mock_container_delete, mock_container_create, @@ -156,9 +156,9 @@ class TestContainerController(api_base.FunctionalTest): self.assertEqual(0, len(c)) self.assertTrue(mock_container_create.called) - @patch('zun.compute.rpcapi.API.container_show') - @patch('zun.compute.rpcapi.API.container_create') - @patch('zun.compute.rpcapi.API.image_search') + @patch('zun.compute.api.API.container_show') + @patch('zun.compute.api.API.container_create') + @patch('zun.compute.api.API.image_search') def test_create_container_without_memory(self, mock_search, mock_container_create, mock_container_show): @@ -187,9 +187,9 @@ class TestContainerController(api_base.FunctionalTest): self.assertEqual({"key1": "val1", "key2": "val2"}, c.get('environment')) - @patch('zun.compute.rpcapi.API.container_show') - @patch('zun.compute.rpcapi.API.container_create') - @patch('zun.compute.rpcapi.API.image_search') + @patch('zun.compute.api.API.container_show') + @patch('zun.compute.api.API.container_create') + @patch('zun.compute.api.API.image_search') def test_create_container_without_environment(self, mock_search, mock_container_create, mock_container_show): @@ -216,9 +216,9 @@ class TestContainerController(api_base.FunctionalTest): self.assertEqual('512M', c.get('memory')) self.assertEqual({}, c.get('environment')) - @patch('zun.compute.rpcapi.API.container_show') - @patch('zun.compute.rpcapi.API.container_create') - @patch('zun.compute.rpcapi.API.image_search') + @patch('zun.compute.api.API.container_show') + @patch('zun.compute.api.API.container_create') + @patch('zun.compute.api.API.image_search') def test_create_container_without_name(self, mock_search, mock_container_create, mock_container_show): @@ -246,8 +246,8 @@ class TestContainerController(api_base.FunctionalTest): self.assertEqual({"key1": "val1", "key2": "val2"}, c.get('environment')) - @patch('zun.compute.rpcapi.API.container_create') - @patch('zun.compute.rpcapi.API.image_search') + @patch('zun.compute.api.API.container_create') + @patch('zun.compute.api.API.image_search') def test_create_container_invalid_long_name(self, mock_search, mock_container_create): # Long name @@ -257,7 +257,7 @@ class TestContainerController(api_base.FunctionalTest): params=params, content_type='application/json') self.assertTrue(mock_container_create.not_called) - @patch('zun.compute.rpcapi.API.container_show') + @patch('zun.compute.api.API.container_show') @patch('zun.objects.Container.list') def test_get_all_containers(self, mock_container_list, mock_container_show): @@ -277,7 +277,7 @@ class TestContainerController(api_base.FunctionalTest): self.assertEqual(test_container['uuid'], actual_containers[0].get('uuid')) - @patch('zun.compute.rpcapi.API.container_show') + @patch('zun.compute.api.API.container_show') @patch('zun.objects.Container.list') def test_get_all_has_status_reason_and_image_pull_policy( self, mock_container_list, mock_container_show): @@ -295,7 +295,7 @@ class TestContainerController(api_base.FunctionalTest): self.assertIn('status_reason', actual_containers[0].keys()) self.assertIn('image_pull_policy', actual_containers[0].keys()) - @patch('zun.compute.rpcapi.API.container_show') + @patch('zun.compute.api.API.container_show') @patch('zun.objects.Container.list') def test_get_all_containers_with_pagination_marker(self, mock_container_list, @@ -318,7 +318,7 @@ class TestContainerController(api_base.FunctionalTest): self.assertEqual(container_list[-1].uuid, actual_containers[0].get('uuid')) - @patch('zun.compute.rpcapi.API.container_show') + @patch('zun.compute.api.API.container_show') @patch('zun.objects.Container.list') def test_get_all_containers_with_exception(self, mock_container_list, mock_container_show): @@ -341,7 +341,7 @@ class TestContainerController(api_base.FunctionalTest): self.assertEqual(fields.ContainerStatus.UNKNOWN, actual_containers[0].get('status')) - @patch('zun.compute.rpcapi.API.container_show') + @patch('zun.compute.api.API.container_show') @patch('zun.objects.Container.get_by_uuid') def test_get_one_by_uuid(self, mock_container_get_by_uuid, mock_container_show): @@ -359,7 +359,7 @@ class TestContainerController(api_base.FunctionalTest): self.assertEqual(test_container['uuid'], response.json['uuid']) - @patch('zun.compute.rpcapi.API.container_show') + @patch('zun.compute.api.API.container_show') @patch('zun.objects.Container.get_by_name') def test_get_one_by_name(self, mock_container_get_by_name, mock_container_show): @@ -439,7 +439,7 @@ class TestContainerController(api_base.FunctionalTest): mock.ANY, test_container_obj) @patch('zun.common.utils.validate_container_state') - @patch('zun.compute.rpcapi.API.container_start') + @patch('zun.compute.api.API.container_start') def test_start_by_uuid(self, mock_container_start, mock_validate): test_container_obj = objects.Container(self.context, **utils.get_test_container()) @@ -458,7 +458,7 @@ class TestContainerController(api_base.FunctionalTest): 'start')) @patch('zun.common.utils.validate_container_state') - @patch('zun.compute.rpcapi.API.container_start') + @patch('zun.compute.api.API.container_start') def test_start_by_name(self, mock_container_start, mock_validate): test_container_obj = objects.Container(self.context, **utils.get_test_container()) @@ -468,7 +468,7 @@ class TestContainerController(api_base.FunctionalTest): mock_container_start, 202) @patch('zun.common.utils.validate_container_state') - @patch('zun.compute.rpcapi.API.container_stop') + @patch('zun.compute.api.API.container_stop') def test_stop_by_uuid(self, mock_container_stop, mock_validate): test_container_obj = objects.Container(self.context, **utils.get_test_container()) @@ -479,7 +479,7 @@ class TestContainerController(api_base.FunctionalTest): query_param='timeout=10') @patch('zun.common.utils.validate_container_state') - @patch('zun.compute.rpcapi.API.container_stop') + @patch('zun.compute.api.API.container_stop') def test_stop_by_name(self, mock_container_stop, mock_validate): test_container_obj = objects.Container(self.context, **utils.get_test_container()) @@ -499,7 +499,7 @@ class TestContainerController(api_base.FunctionalTest): 'stop')) @patch('zun.common.utils.validate_container_state') - @patch('zun.compute.rpcapi.API.container_pause') + @patch('zun.compute.api.API.container_pause') def test_pause_by_uuid(self, mock_container_pause, mock_validate): test_container_obj = objects.Container(self.context, **utils.get_test_container()) @@ -518,7 +518,7 @@ class TestContainerController(api_base.FunctionalTest): 'pause')) @patch('zun.common.utils.validate_container_state') - @patch('zun.compute.rpcapi.API.container_pause') + @patch('zun.compute.api.API.container_pause') def test_pause_by_name(self, mock_container_pause, mock_validate): test_container_obj = objects.Container(self.context, **utils.get_test_container()) @@ -528,7 +528,7 @@ class TestContainerController(api_base.FunctionalTest): mock_container_pause, 202) @patch('zun.common.utils.validate_container_state') - @patch('zun.compute.rpcapi.API.container_unpause') + @patch('zun.compute.api.API.container_unpause') def test_unpause_by_uuid(self, mock_container_unpause, mock_validate): test_container_obj = objects.Container(self.context, **utils.get_test_container()) @@ -548,7 +548,7 @@ class TestContainerController(api_base.FunctionalTest): 'unpause')) @patch('zun.common.utils.validate_container_state') - @patch('zun.compute.rpcapi.API.container_unpause') + @patch('zun.compute.api.API.container_unpause') def test_unpause_by_name(self, mock_container_unpause, mock_validate): test_container_obj = objects.Container(self.context, **utils.get_test_container()) @@ -558,7 +558,7 @@ class TestContainerController(api_base.FunctionalTest): mock_container_unpause, 202) @patch('zun.common.utils.validate_container_state') - @patch('zun.compute.rpcapi.API.container_reboot') + @patch('zun.compute.api.API.container_reboot') def test_reboot_by_uuid(self, mock_container_reboot, mock_validate): test_container_obj = objects.Container(self.context, **utils.get_test_container()) @@ -578,7 +578,7 @@ class TestContainerController(api_base.FunctionalTest): 'reboot')) @patch('zun.common.utils.validate_container_state') - @patch('zun.compute.rpcapi.API.container_reboot') + @patch('zun.compute.api.API.container_reboot') def test_reboot_by_name(self, mock_container_reboot, mock_validate): test_container_obj = objects.Container(self.context, **utils.get_test_container()) @@ -588,7 +588,7 @@ class TestContainerController(api_base.FunctionalTest): mock_container_reboot, 202, query_param='timeout=10') - @patch('zun.compute.rpcapi.API.container_logs') + @patch('zun.compute.api.API.container_logs') @patch('zun.objects.Container.get_by_uuid') def test_get_logs_by_uuid(self, mock_get_by_uuid, mock_container_logs): mock_container_logs.return_value = "test" @@ -603,7 +603,7 @@ class TestContainerController(api_base.FunctionalTest): mock_container_logs.assert_called_once_with( mock.ANY, test_container_obj) - @patch('zun.compute.rpcapi.API.container_logs') + @patch('zun.compute.api.API.container_logs') @patch('zun.objects.Container.get_by_name') def test_get_logs_by_name(self, mock_get_by_name, mock_container_logs): mock_container_logs.return_value = "test logs" @@ -618,7 +618,7 @@ class TestContainerController(api_base.FunctionalTest): mock_container_logs.assert_called_once_with( mock.ANY, test_container_obj) - @patch('zun.compute.rpcapi.API.container_logs') + @patch('zun.compute.api.API.container_logs') @patch('zun.objects.Container.get_by_uuid') def test_get_logs_put_fails(self, mock_get_by_uuid, mock_container_logs): test_container = utils.get_test_container() @@ -631,7 +631,7 @@ class TestContainerController(api_base.FunctionalTest): self.assertFalse(mock_container_logs.called) @patch('zun.common.utils.validate_container_state') - @patch('zun.compute.rpcapi.API.container_exec') + @patch('zun.compute.api.API.container_exec') @patch('zun.objects.Container.get_by_uuid') def test_execute_command_by_uuid(self, mock_get_by_uuid, mock_container_exec, mock_validate): @@ -660,7 +660,7 @@ class TestContainerController(api_base.FunctionalTest): 'execute'), cmd) @patch('zun.common.utils.validate_container_state') - @patch('zun.compute.rpcapi.API.container_exec') + @patch('zun.compute.api.API.container_exec') @patch('zun.objects.Container.get_by_name') def test_execute_command_by_name(self, mock_get_by_name, mock_container_exec, mock_validate): @@ -678,7 +678,7 @@ class TestContainerController(api_base.FunctionalTest): mock.ANY, test_container_obj, cmd['command']) @patch('zun.common.utils.validate_container_state') - @patch('zun.compute.rpcapi.API.container_delete') + @patch('zun.compute.api.API.container_delete') @patch('zun.objects.Container.get_by_uuid') def test_delete_container_by_uuid(self, mock_get_by_uuid, mock_container_delete, mock_validate): @@ -704,7 +704,7 @@ class TestContainerController(api_base.FunctionalTest): "Cannot delete container %s in Running state" % uuid): self.app.delete('/v1/containers/%s' % (test_object.uuid)) - @patch('zun.compute.rpcapi.API.container_delete') + @patch('zun.compute.api.API.container_delete') def test_delete_by_uuid_invalid_state_force_true(self, mock_delete): uuid = uuidutils.generate_uuid() test_object = utils.create_test_container(context=self.context, @@ -714,7 +714,7 @@ class TestContainerController(api_base.FunctionalTest): self.assertEqual(204, response.status_int) @patch('zun.common.utils.validate_container_state') - @patch('zun.compute.rpcapi.API.container_delete') + @patch('zun.compute.api.API.container_delete') @patch('zun.objects.Container.get_by_name') def test_delete_container_by_name(self, mock_get_by_name, mock_container_delete, mock_validate): @@ -732,7 +732,7 @@ class TestContainerController(api_base.FunctionalTest): mock_destroy.assert_called_once_with(mock.ANY) @patch('zun.common.utils.validate_container_state') - @patch('zun.compute.rpcapi.API.container_kill') + @patch('zun.compute.api.API.container_kill') @patch('zun.objects.Container.get_by_uuid') def test_kill_container_by_uuid(self, mock_get_by_uuid, mock_container_kill, @@ -763,7 +763,7 @@ class TestContainerController(api_base.FunctionalTest): 'kill'), body) @patch('zun.common.utils.validate_container_state') - @patch('zun.compute.rpcapi.API.container_kill') + @patch('zun.compute.api.API.container_kill') @patch('zun.objects.Container.get_by_name') def test_kill_container_by_name(self, mock_get_by_name, mock_container_kill, @@ -784,7 +784,7 @@ class TestContainerController(api_base.FunctionalTest): mock.ANY, test_container_obj, cmd['signal']) @patch('zun.common.utils.validate_container_state') - @patch('zun.compute.rpcapi.API.container_kill') + @patch('zun.compute.api.API.container_kill') @patch('zun.objects.Container.get_by_uuid') def test_kill_container_which_not_exist(self, mock_get_by_uuid, @@ -802,7 +802,7 @@ class TestContainerController(api_base.FunctionalTest): self.assertTrue(mock_container_kill.called) @patch('zun.common.utils.validate_container_state') - @patch('zun.compute.rpcapi.API.container_kill') + @patch('zun.compute.api.API.container_kill') @patch('zun.objects.Container.get_by_uuid') def test_kill_container_with_exception(self, mock_get_by_uuid, diff --git a/zun/tests/unit/api/controllers/v1/test_images.py b/zun/tests/unit/api/controllers/v1/test_images.py index 9ccf6496a..aec2804d8 100644 --- a/zun/tests/unit/api/controllers/v1/test_images.py +++ b/zun/tests/unit/api/controllers/v1/test_images.py @@ -23,7 +23,7 @@ from zun.tests.unit.db import utils class TestImageController(api_base.FunctionalTest): - @patch('zun.compute.rpcapi.API.image_pull') + @patch('zun.compute.api.API.image_pull') def test_image_pull(self, mock_image_pull): mock_image_pull.side_effect = lambda x, y: y @@ -43,7 +43,7 @@ class TestImageController(api_base.FunctionalTest): self.assertEqual(202, response.status_int) self.assertTrue(mock_image_pull.called) - @patch('zun.compute.rpcapi.API.image_pull') + @patch('zun.compute.api.API.image_pull') def test_image_pull_with_no_repo(self, mock_image_pull): params = {} with self.assertRaisesRegexp(AppError, @@ -53,7 +53,7 @@ class TestImageController(api_base.FunctionalTest): content_type='application/json') self.assertTrue(mock_image_pull.not_called) - @patch('zun.compute.rpcapi.API.image_pull') + @patch('zun.compute.api.API.image_pull') def test_image_pull_conflict(self, mock_image_pull): mock_image_pull.side_effect = lambda x, y: y @@ -68,7 +68,7 @@ class TestImageController(api_base.FunctionalTest): params=params, content_type='application/json') self.assertTrue(mock_image_pull.not_called) - @patch('zun.compute.rpcapi.API.image_pull') + @patch('zun.compute.api.API.image_pull') def test_pull_image_set_project_id_and_user_id( self, mock_image_pull): def _create_side_effect(cnxt, image): @@ -82,7 +82,7 @@ class TestImageController(api_base.FunctionalTest): params=params, content_type='application/json') - @patch('zun.compute.rpcapi.API.image_pull') + @patch('zun.compute.api.API.image_pull') def test_image_pull_with_tag(self, mock_image_pull): mock_image_pull.side_effect = lambda x, y: y @@ -94,7 +94,7 @@ class TestImageController(api_base.FunctionalTest): self.assertEqual(202, response.status_int) self.assertTrue(mock_image_pull.called) - @patch('zun.compute.rpcapi.API.image_show') + @patch('zun.compute.api.API.image_show') @patch('zun.objects.Image.list') def test_get_all_images(self, mock_image_list, mock_image_show): test_image = utils.get_test_image() @@ -113,7 +113,7 @@ class TestImageController(api_base.FunctionalTest): self.assertEqual(test_image['uuid'], actual_images[0].get('uuid')) - @patch('zun.compute.rpcapi.API.image_show') + @patch('zun.compute.api.API.image_show') @patch('zun.objects.Image.list') def test_get_all_images_with_pagination_marker(self, mock_image_list, mock_image_show): @@ -136,7 +136,7 @@ class TestImageController(api_base.FunctionalTest): self.assertEqual(image_list[-1].uuid, actual_images[0].get('uuid')) - @patch('zun.compute.rpcapi.API.image_show') + @patch('zun.compute.api.API.image_show') @patch('zun.objects.Image.list') def test_get_all_images_with_exception(self, mock_image_list, mock_image_show): @@ -156,28 +156,28 @@ class TestImageController(api_base.FunctionalTest): self.assertEqual(test_image['uuid'], actual_images[0].get('uuid')) - @patch('zun.compute.rpcapi.API.image_search') + @patch('zun.compute.api.API.image_search') def test_search_image(self, mock_image_search): mock_image_search.return_value = {'name': 'redis', 'stars': 2000} response = self.app.get('/v1/images/redis/search/') self.assertEqual(200, response.status_int) mock_image_search.assert_called_once_with( - mock.ANY, 'redis', exact_match=False) + mock.ANY, 'redis', False) - @patch('zun.compute.rpcapi.API.image_search') + @patch('zun.compute.api.API.image_search') def test_search_image_with_tag(self, mock_image_search): mock_image_search.return_value = {'name': 'redis', 'stars': 2000} response = self.app.get('/v1/images/redis:test/search/') self.assertEqual(200, response.status_int) mock_image_search.assert_called_once_with( - mock.ANY, 'redis:test', exact_match=False) + mock.ANY, 'redis:test', False) - @patch('zun.compute.rpcapi.API.image_search') + @patch('zun.compute.api.API.image_search') def test_search_image_not_found(self, mock_image_search): mock_image_search.side_effect = exception.ImageNotFound self.assertRaises(AppError, self.app.get, '/v1/images/redis/search/') mock_image_search.assert_called_once_with( - mock.ANY, 'redis', exact_match=False) + mock.ANY, 'redis', False) class TestImageEnforcement(api_base.FunctionalTest): diff --git a/zun/tests/unit/common/test_context.py b/zun/tests/unit/common/test_context.py index 60d84bb15..82d59eca2 100644 --- a/zun/tests/unit/common/test_context.py +++ b/zun/tests/unit/common/test_context.py @@ -96,3 +96,13 @@ class ContextTestCase(base.TestCase): def test_request_context_sets_is_admin(self): ctxt = zun_context.get_admin_context() self.assertTrue(ctxt.is_admin) + + def test_request_context_elevated(self): + ctx = self._create_context(is_admin=False, roles=['Member']) + + self.assertFalse(ctx.is_admin) + admin_ctxt = ctx.elevated() + self.assertTrue(admin_ctxt.is_admin) + self.assertIn('admin', admin_ctxt.roles) + self.assertFalse(ctx.is_admin) + self.assertNotIn('admin', ctx.roles) diff --git a/zun/tests/unit/container/docker/test_docker_driver.py b/zun/tests/unit/container/docker/test_docker_driver.py index 605e6cc44..951043e51 100644 --- a/zun/tests/unit/container/docker/test_docker_driver.py +++ b/zun/tests/unit/container/docker/test_docker_driver.py @@ -13,6 +13,7 @@ from docker import errors import mock +from zun import conf from zun.container.docker.driver import DockerDriver from zun.container.docker.driver import NovaDockerDriver from zun.container.docker import utils as docker_utils @@ -432,14 +433,16 @@ class TestNovaDockerDriver(base.DriverTestCase): mock_ensure_active.return_value = True mock_find_container_by_server_name.return_value = \ 'test_container_name_id' - mock_container = mock.MagicMock() + db_container = db_utils.create_test_container(context=self.context, + host=conf.CONF.host) + mock_container = mock.MagicMock(**db_container) result_sandbox_id = self.driver.create_sandbox(self.context, mock_container) mock_get_sandbox_name.assert_called_once_with(mock_container) nova_client_instance.create_server.assert_called_once_with( name='test_sanbox_name', image='kubernetes/pause', flavor='m1.small', key_name=None, - nics='auto') + nics='auto', availability_zone=':{0}:'.format(conf.CONF.host)) mock_ensure_active.assert_called_once_with(nova_client_instance, 'server_instance') mock_find_container_by_server_name.assert_called_once_with( diff --git a/zun/tests/unit/db/test_zun_service.py b/zun/tests/unit/db/test_zun_service.py index 1015de1ac..e7559eecd 100644 --- a/zun/tests/unit/db/test_zun_service.py +++ b/zun/tests/unit/db/test_zun_service.py @@ -27,6 +27,105 @@ from zun.tests.unit.db.utils import FakeEtcdMultipleResult from zun.tests.unit.db.utils import FakeEtcdResult +class DbZunServiceTestCase(base.DbTestCase): + + def test_create_zun_service(self): + utils.create_test_zun_service() + + def test_create_zun_service_failure_for_dup(self): + utils.create_test_zun_service() + self.assertRaises(exception.ZunServiceAlreadyExists, + utils.create_test_zun_service) + + def test_get_zun_service(self): + ms = utils.create_test_zun_service() + res = self.dbapi.get_zun_service( + ms['host'], ms['binary']) + self.assertEqual(ms.id, res.id) + + def test_get_zun_service_failure(self): + utils.create_test_zun_service() + res = self.dbapi.get_zun_service( + 'fakehost1', 'fake-bin1') + self.assertIsNone(res) + + def test_update_zun_service(self): + ms = utils.create_test_zun_service() + d2 = True + update = {'disabled': d2} + ms1 = self.dbapi.update_zun_service(ms['host'], ms['binary'], update) + self.assertEqual(ms['id'], ms1['id']) + self.assertEqual(d2, ms1['disabled']) + res = self.dbapi.get_zun_service( + 'fakehost', 'fake-bin') + self.assertEqual(ms1['id'], res['id']) + self.assertEqual(d2, res['disabled']) + + def test_update_zun_service_failure(self): + fake_update = {'fake_field': 'fake_value'} + self.assertRaises(exception.ZunServiceNotFound, + self.dbapi.update_zun_service, + 'fakehost1', 'fake-bin1', fake_update) + + def test_destroy_zun_service(self): + ms = utils.create_test_zun_service() + res = self.dbapi.get_zun_service( + 'fakehost', 'fake-bin') + self.assertEqual(res['id'], ms['id']) + self.dbapi.destroy_zun_service(ms['host'], ms['binary']) + res = self.dbapi.get_zun_service( + 'fakehost', 'fake-bin') + self.assertIsNone(res) + + def test_destroy_zun_service_failure(self): + self.assertRaises(exception.ZunServiceNotFound, + self.dbapi.destroy_zun_service, + 'fakehostsssss', 'fakessss-bin1') + + def test_get_zun_service_list(self): + fake_ms_params = { + 'report_count': 1010, + 'host': 'FakeHost', + 'binary': 'FakeBin', + 'disabled': False, + 'disabled_reason': 'FakeReason' + } + utils.create_test_zun_service(**fake_ms_params) + res = self.dbapi.get_zun_service_list() + self.assertEqual(1, len(res)) + res = res[0] + for k, v in fake_ms_params.items(): + self.assertEqual(res[k], v) + + fake_ms_params['binary'] = 'FakeBin1' + fake_ms_params['disabled'] = True + utils.create_test_zun_service(**fake_ms_params) + res = self.dbapi.get_zun_service_list(disabled=True) + self.assertEqual(1, len(res)) + res = res[0] + for k, v in fake_ms_params.items(): + self.assertEqual(res[k], v) + + def test_list_zun_service_by_binary(self): + fake_ms_params = { + 'report_count': 1010, + 'host': 'FakeHost', + 'binary': 'FakeBin', + 'disabled': False, + 'disabled_reason': 'FakeReason' + } + utils.create_test_zun_service(**fake_ms_params) + res = self.dbapi.list_zun_service_by_binary( + binary=fake_ms_params['binary']) + self.assertEqual(1, len(res)) + res = res[0] + for k, v in fake_ms_params.items(): + self.assertEqual(res[k], v) + + res = self.dbapi.list_zun_service_by_binary(binary='none') + self.assertEqual(0, len(res)) + + class EtcdDbZunServiceTestCase(base.DbTestCase): def setUp(self): diff --git a/zun/tests/unit/objects/test_zun_service.py b/zun/tests/unit/objects/test_zun_service.py index 87d4283a0..6013c09ae 100644 --- a/zun/tests/unit/objects/test_zun_service.py +++ b/zun/tests/unit/objects/test_zun_service.py @@ -56,6 +56,16 @@ class TestZunServiceObject(base.DbTestCase): self.assertIsInstance(services[0], objects.ZunService) self.assertEqual(self.context, services[0]._context) + def test_list_by_binary(self): + with mock.patch.object(self.dbapi, 'list_zun_service_by_binary', + autospec=True) as mock_service_list: + mock_service_list.return_value = [self.fake_zun_service] + services = objects.ZunService.list_by_binary(self.context, 'bin') + self.assertEqual(1, mock_service_list.call_count) + self.assertThat(services, HasLength(1)) + self.assertIsInstance(services[0], objects.ZunService) + self.assertEqual(self.context, services[0]._context) + def test_create(self): with mock.patch.object(self.dbapi, 'create_zun_service', autospec=True) as mock_create_zun_service: diff --git a/zun/tests/unit/scheduler/__init__.py b/zun/tests/unit/scheduler/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/zun/tests/unit/scheduler/fake_scheduler.py b/zun/tests/unit/scheduler/fake_scheduler.py new file mode 100644 index 000000000..443fc1662 --- /dev/null +++ b/zun/tests/unit/scheduler/fake_scheduler.py @@ -0,0 +1,19 @@ +# 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 zun.scheduler import driver + + +class FakeScheduler(driver.Scheduler): + + def select_destinations(self, context, containers): + return [] diff --git a/zun/tests/unit/scheduler/test_chance_scheduler.py b/zun/tests/unit/scheduler/test_chance_scheduler.py new file mode 100644 index 000000000..a3df307a6 --- /dev/null +++ b/zun/tests/unit/scheduler/test_chance_scheduler.py @@ -0,0 +1,61 @@ +# 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 mock + +from zun.common import exception +from zun import objects +from zun.scheduler import chance_scheduler +from zun.tests import base +from zun.tests.unit.db import utils + + +class ChanceSchedulerTestCase(base.TestCase): + """Test case for Chance Scheduler.""" + + driver_cls = chance_scheduler.ChanceScheduler + + @mock.patch.object(driver_cls, 'hosts_up') + @mock.patch('random.choice') + def test_select_destinations(self, mock_random_choice, mock_hosts_up): + all_hosts = ['host1', 'host2', 'host3', 'host4'] + + def _return_hosts(*args, **kwargs): + return all_hosts + + mock_random_choice.side_effect = ['host3'] + mock_hosts_up.side_effect = _return_hosts + + test_container = utils.get_test_container() + containers = [objects.Container(self.context, **test_container)] + dests = self.driver_cls().select_destinations(self.context, containers) + + self.assertEqual(1, len(dests)) + (host, node) = (dests[0]['host'], dests[0]['nodename']) + self.assertEqual('host3', host) + self.assertIsNone(node) + + calls = [mock.call(all_hosts)] + self.assertEqual(calls, mock_random_choice.call_args_list) + + @mock.patch.object(driver_cls, 'hosts_up') + def test_select_destinations_no_valid_host(self, mock_hosts_up): + + def _return_no_host(*args, **kwargs): + return [] + + mock_hosts_up.side_effect = _return_no_host + test_container = utils.get_test_container() + containers = [objects.Container(self.context, **test_container)] + self.assertRaises(exception.NoValidHost, + self.driver_cls().select_destinations, self.context, + containers) diff --git a/zun/tests/unit/scheduler/test_client.py b/zun/tests/unit/scheduler/test_client.py new file mode 100644 index 000000000..6f13bd8be --- /dev/null +++ b/zun/tests/unit/scheduler/test_client.py @@ -0,0 +1,47 @@ +# 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 mock + +from oslo_config import cfg + +from zun.scheduler import chance_scheduler +from zun.scheduler import client as scheduler_client +from zun.tests import base +from zun.tests.unit.scheduler import fake_scheduler + + +CONF = cfg.CONF + + +class SchedulerClientTestCase(base.TestCase): + + def setUp(self): + super(SchedulerClientTestCase, self).setUp() + self.client_cls = scheduler_client.SchedulerClient + self.client = self.client_cls() + + def test_init_using_default_schedulerdriver(self): + driver = self.client_cls().driver + self.assertIsInstance(driver, chance_scheduler.ChanceScheduler) + + def test_init_using_custom_schedulerdriver(self): + CONF.set_override('driver', 'fake_scheduler', group='scheduler') + driver = self.client_cls().driver + self.assertIsInstance(driver, fake_scheduler.FakeScheduler) + + @mock.patch('zun.scheduler.chance_scheduler.ChanceScheduler' + '.select_destinations') + def test_select_destinations(self, mock_select_destinations): + fake_args = ['ctxt', 'fake_containers'] + self.client.select_destinations(*fake_args) + mock_select_destinations.assert_called_once_with(*fake_args) diff --git a/zun/tests/unit/scheduler/test_scheduler.py b/zun/tests/unit/scheduler/test_scheduler.py new file mode 100644 index 000000000..abce78b6b --- /dev/null +++ b/zun/tests/unit/scheduler/test_scheduler.py @@ -0,0 +1,48 @@ +# 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. +""" +Tests For Scheduler +""" + +import mock + +from zun import objects +from zun.tests import base +from zun.tests.unit.scheduler import fake_scheduler + + +class SchedulerTestCase(base.TestCase): + """Test case for base scheduler driver class.""" + + driver_cls = fake_scheduler.FakeScheduler + + def setUp(self): + super(SchedulerTestCase, self).setUp() + self.driver = self.driver_cls() + + @mock.patch('zun.objects.ZunService.list_by_binary') + @mock.patch('zun.api.servicegroup.ServiceGroup.service_is_up') + def test_hosts_up(self, mock_service_is_up, mock_list_by_binary): + service1 = objects.ZunService(host='host1') + service2 = objects.ZunService(host='host2') + services = [service1, service2] + + mock_list_by_binary.return_value = services + mock_service_is_up.side_effect = [False, True] + + result = self.driver.hosts_up(self.context) + self.assertEqual(result, ['host2']) + + mock_list_by_binary.assert_called_once_with(self.context, + 'zun-compute') + calls = [mock.call(service1), mock.call(service2)] + self.assertEqual(calls, mock_service_is_up.call_args_list)