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
This commit is contained in:
parent
d907557d0f
commit
ddc2a81532
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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):
|
||||
|
@ -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()
|
||||
|
@ -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)
|
||||
|
@ -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")
|
||||
|
@ -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)
|
||||
|
94
zun/compute/api.py
Normal file
94
zun/compute/api.py
Normal file
@ -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)
|
@ -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)
|
||||
|
@ -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)
|
||||
|
49
zun/conf/scheduler.py
Normal file
49
zun/conf/scheduler.py
Normal file
@ -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}
|
@ -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 '
|
||||
|
@ -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)
|
||||
|
@ -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.
|
||||
|
@ -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'):
|
||||
|
@ -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.
|
||||
|
0
zun/scheduler/__init__.py
Normal file
0
zun/scheduler/__init__.py
Normal file
48
zun/scheduler/chance_scheduler.py
Normal file
48
zun/scheduler/chance_scheduler.py
Normal file
@ -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
|
33
zun/scheduler/client.py
Normal file
33
zun/scheduler/client.py
Normal file
@ -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)
|
55
zun/scheduler/driver.py
Normal file
55
zun/scheduler/driver.py
Normal file
@ -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 []
|
@ -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,
|
||||
|
@ -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):
|
||||
|
@ -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)
|
||||
|
@ -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(
|
||||
|
@ -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):
|
||||
|
@ -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:
|
||||
|
0
zun/tests/unit/scheduler/__init__.py
Normal file
0
zun/tests/unit/scheduler/__init__.py
Normal file
19
zun/tests/unit/scheduler/fake_scheduler.py
Normal file
19
zun/tests/unit/scheduler/fake_scheduler.py
Normal file
@ -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 []
|
61
zun/tests/unit/scheduler/test_chance_scheduler.py
Normal file
61
zun/tests/unit/scheduler/test_chance_scheduler.py
Normal file
@ -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)
|
47
zun/tests/unit/scheduler/test_client.py
Normal file
47
zun/tests/unit/scheduler/test_client.py
Normal file
@ -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)
|
48
zun/tests/unit/scheduler/test_scheduler.py
Normal file
48
zun/tests/unit/scheduler/test_scheduler.py
Normal file
@ -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)
|
Loading…
Reference in New Issue
Block a user