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:
Hongbin Lu 2017-01-11 10:21:32 -06:00
parent d907557d0f
commit ddc2a81532
32 changed files with 821 additions and 156 deletions

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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):

View File

@ -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()

View File

@ -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)

View File

@ -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")

View File

@ -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
View 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)

View File

@ -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)

View File

@ -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
View 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}

View File

@ -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 '

View File

@ -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)

View File

@ -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.

View File

@ -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'):

View File

@ -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.

View File

View 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
View 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
View 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 []

View File

@ -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,

View File

@ -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):

View File

@ -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)

View File

@ -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(

View File

@ -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):

View File

@ -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:

View File

View 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 []

View 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)

View 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)

View 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)