Import nova filter scheduler to zun

Import nova filter scheduler to zun.
Add unittest in next patch

Change-Id: Ibf6e80bd9714a584d625852aea1afca57086aa5b
Partially-Implements: blueprint import-nova-filter-scheduler
This commit is contained in:
ShunliZhou 2017-03-14 16:56:21 +08:00
parent 92387ce610
commit dd63b06a4c
8 changed files with 480 additions and 1 deletions

View File

@ -63,6 +63,7 @@ zun.database.migration_backend =
zun.scheduler.driver =
chance_scheduler = zun.scheduler.chance_scheduler:ChanceScheduler
fake_scheduler = zun.tests.unit.scheduler.fake_scheduler:FakeScheduler
filter_scheduler = zun.scheduler.filter_scheduler:FilterScheduler
zun.image.driver =
glance = zun.image.glance.driver:GlanceDriver

View File

@ -454,3 +454,16 @@ class CPUPinningInvalid(Invalid):
class CPUUnpinningInvalid(Invalid):
msg_fmt = _("CPU set to unpin %(requested)s must be a subset of "
"pinned CPU set %(pinned)s")
class NotFound(ZunException):
msg_fmt = _("Resource could not be found.")
code = 404
class SchedulerHostFilterNotFound(NotFound):
msg_fmt = _("Scheduler Host Filter %(filter_name)s could not be found.")
class ClassNotFound(NotFound):
msg_fmt = _("Class %(class_name)s could not be found: %(exception)s")

View File

@ -22,7 +22,8 @@ scheduler_group = cfg.OptGroup(name="scheduler",
scheduler_opts = [
cfg.StrOpt("driver",
default="chance_scheduler",
choices=("chance_scheduler", "fake_scheduler"),
choices=("chance_scheduler", "fake_scheduler",
"filter_scheduler"),
help="""
The class of the driver used by the scheduler.
@ -36,6 +37,56 @@ Possible values:
** '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
"""),
cfg.MultiStrOpt("available_filters",
default=["zun.scheduler.filters.all_filters"],
help="""
Filters that the scheduler can use.
An unordered list of the filter classes the zun scheduler may apply. Only the
filters specified in the 'scheduler_enabled_filters' option will be used, but
any filter appearing in that option must also be included in this list.
By default, this is set to all filters that are included with zun.
This option is only used by the FilterScheduler and its subclasses; if you use
a different scheduler, this option has no effect.
Possible values:
* A list of zero or more strings, where each string corresponds to the name of
a filter that may be used for selecting a host
Related options:
* scheduler_enabled_filters
"""),
cfg.ListOpt("enabled_filters",
default=[
"NoopFilter",
],
help="""
Filters that the scheduler will use.
An ordered list of filter class names that will be used for filtering
hosts. Ignore the word 'default' in the name of this option: these filters will
*always* be applied, and they will be applied in the order they are listed so
place your most restrictive filters first to make the filtering process more
efficient.
This option is only used by the FilterScheduler and its subclasses; if you use
a different scheduler, this option has no effect.
Possible values:
* A list of zero or more strings, where each string corresponds to the name of
a filter to be used for selecting a host
Related options:
* All of the filters in this option *must* be present in the
'scheduler_available_filters' option, or a SchedulerHostFilterNotFound
exception will be raised.
"""),
]

View File

@ -0,0 +1,111 @@
# 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.
"""
Filter support
"""
from oslo_log import log as logging
from zun.common.i18n import _LI
from zun.scheduler import loadables
LOG = logging.getLogger(__name__)
class BaseFilter(object):
"""Base class for all filter classes."""
def _filter_one(self, obj, container):
"""Return True if it passes the filter, False otherwise."""
return True
def filter_all(self, filter_obj_list, container):
"""Yield objects that pass the filter.
Can be overridden in a subclass, if you need to base filtering
decisions on all objects. Otherwise, one can just override
_filter_one() to filter a single object.
"""
for obj in filter_obj_list:
if self._filter_one(obj, container):
yield obj
# Set to true in a subclass if a filter only needs to be run once
# for each request rather than for each instance
run_filter_once_per_request = False
def run_filter_for_index(self, index):
"""Return True or False,
if the filter needs to be run for the "index-th" instance in a
request, return True. Only need to override this if a filter
needs anything other than "first only" or "all" behaviour.
"""
if self.run_filter_once_per_request and index > 0:
return False
else:
return True
class BaseFilterHandler(loadables.BaseLoader):
"""Base class to handle loading filter classes.
This class should be subclassed where one needs to use filters.
"""
def get_filtered_objects(self, filters, objs, container, index=0):
list_objs = list(objs)
LOG.debug("Starting with %d host(s)", len(list_objs))
part_filter_results = []
full_filter_results = []
log_msg = "%(cls_name)s: (start: %(start)s, end: %(end)s)"
for filter_ in filters:
if filter_.run_filter_for_index(index):
cls_name = filter_.__class__.__name__
start_count = len(list_objs)
objs = filter_.filter_all(list_objs, container)
if objs is None:
LOG.debug("Filter %s says to stop filtering", cls_name)
return
list_objs = list(objs)
end_count = len(list_objs)
part_filter_results.append(log_msg % {"cls_name": cls_name,
"start": start_count,
"end": end_count})
if list_objs:
remaining = [(getattr(obj, "host", obj),
getattr(obj, "nodename", ""))
for obj in list_objs]
full_filter_results.append((cls_name, remaining))
else:
LOG.info(_LI("Filter %s returned 0 hosts"), cls_name)
full_filter_results.append((cls_name, None))
break
LOG.debug("Filter %(cls_name)s returned "
"%(obj_len)d host(s)",
{'cls_name': cls_name, 'obj_len': len(list_objs)})
if not list_objs:
cnt_uuid = container.uuid
msg_dict = {"cnt_uuid": cnt_uuid,
"str_results": str(full_filter_results)}
full_msg = ("Filtering removed all hosts for the request with "
"container ID "
"'%(cnt_uuid)s'. Filter results: %(str_results)s"
) % msg_dict
msg_dict["str_results"] = str(part_filter_results)
part_msg = _LI("Filtering removed all hosts for the request with "
"container ID "
"'%(cnt_uuid)s'. Filter results: %(str_results)s"
) % msg_dict
LOG.debug(full_msg)
LOG.info(part_msg)
return list_objs

View File

@ -0,0 +1,99 @@
# 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.
"""
The FilterScheduler is for scheduling container to a host according to
your filters configured.
You can customize this scheduler by specifying your own Host Filters.
"""
import random
from zun.common import exception
from zun.common.i18n import _
import zun.conf
from zun import objects
from zun.scheduler import driver
from zun.scheduler import filters
CONF = zun.conf.CONF
class FilterScheduler(driver.Scheduler):
"""Scheduler that can be used for filtering zun compute."""
def __init__(self, *args, **kwargs):
super(FilterScheduler, self).__init__(*args, **kwargs)
self.filter_handler = filters.HostFilterHandler()
filter_classes = self.filter_handler.get_matching_classes(
CONF.scheduler.available_filters)
self.filter_cls_map = {cls.__name__: cls for cls in filter_classes}
self.filter_obj_map = {}
self.enabled_filters = self._choose_host_filters(self._load_filters())
def _schedule(self, context, container):
"""Picks a host according to filters."""
services = objects.ZunService.list_by_binary(context, 'zun-compute')
hosts = [service.host
for service in services
if self.servicegroup_api.service_is_up(service)]
hosts = self.filter_handler.get_filtered_objects(self.enabled_filters,
hosts,
container)
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 destinations by filters."""
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
def _choose_host_filters(self, filter_cls_names):
"""Choose good filters
Since the caller may specify which filters to use we need
to have an authoritative list of what is permissible. This
function checks the filter names against a predefined set
of acceptable filters.
"""
if not isinstance(filter_cls_names, (list, tuple)):
filter_cls_names = [filter_cls_names]
good_filters = []
bad_filters = []
for filter_name in filter_cls_names:
if filter_name not in self.filter_obj_map:
if filter_name not in self.filter_cls_map:
bad_filters.append(filter_name)
continue
filter_cls = self.filter_cls_map[filter_name]
self.filter_obj_map[filter_name] = filter_cls()
good_filters.append(self.filter_obj_map[filter_name])
if bad_filters:
msg = ", ".join(bad_filters)
raise exception.SchedulerHostFilterNotFound(filter_name=msg)
return good_filters
def _load_filters(self):
return CONF.scheduler.enabled_filters

View File

@ -0,0 +1,45 @@
# 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 host filters
"""
from zun.scheduler import base_filters
class BaseHostFilter(base_filters.BaseFilter):
"""Base class for host filters."""
def _filter_one(self, obj, filter_properties):
"""Return True if the object passes the filter, otherwise False."""
return self.host_passes(obj, filter_properties)
def host_passes(self, host_state, filter_properties):
"""Return True if the HostState passes the filter,otherwise False.
Override this in a subclass.
"""
raise NotImplementedError()
class HostFilterHandler(base_filters.BaseFilterHandler):
def __init__(self):
super(HostFilterHandler, self).__init__(BaseHostFilter)
def all_filters():
"""Return a list of filter classes found in this directory.
This method is used as the default for available scheduler filters
and should return a list of all filter classes available.
"""
return HostFilterHandler().get_all_classes()

View File

@ -0,0 +1,38 @@
# Copyright (c) 2017 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_log import log as logging
from zun.api import servicegroup
from zun.scheduler import filters
LOG = logging.getLogger(__name__)
class NoopFilter(filters.BaseHostFilter):
"""Noop filter for now"""
def __init__(self):
self.servicegroup_api = servicegroup.ServiceGroup()
# Host state does not change within a request
run_filter_once_per_request = True
def host_passes(self, host_state, container):
"""Noop filter for now"""
# Depend on the objects.NodeInfo of below patch to filter node,
# https://review.openstack.org/#/c/436572/6, no more thing can do now.
return True

121
zun/scheduler/loadables.py Normal file
View File

@ -0,0 +1,121 @@
# 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.
"""
Generic Loadable class support.
Meant to be used by such things as scheduler filters where we
want to load modules from certain directories and find certain types of
classes within those modules. Note that this is quite different than
generic plugins and the pluginmanager code that exists elsewhere.
Usage:
Create a directory with an __init__.py with code such as:
class SomeLoadableClass(object):
pass
class MyLoader(zun.loadables.BaseLoader)
def __init__(self):
super(MyLoader, self).__init__(SomeLoadableClass)
If you create modules in the same directory and subclass SomeLoadableClass
within them, MyLoader().get_all_classes() will return a list
of such classes.
"""
import inspect
import os
import sys
from oslo_utils import importutils
from zun.common import exception
class BaseLoader(object):
def __init__(self, loadable_cls_type):
mod = sys.modules[self.__class__.__module__]
self.path = os.path.abspath(mod.__path__[0])
self.package = mod.__package__
self.loadable_cls_type = loadable_cls_type
def _is_correct_class(self, obj):
"""Judge whether class type is correct
Return whether an object is a class of the correct type and
is not prefixed with an underscore.
"""
return (inspect.isclass(obj) and
(not obj.__name__.startswith('_')) and
issubclass(obj, self.loadable_cls_type))
def _get_classes_from_module(self, module_name):
"""Get the classes from a module that match the type we want."""
classes = []
module = importutils.import_module(module_name)
for obj_name in dir(module):
# Skip objects that are meant to be private.
if obj_name.startswith('_'):
continue
itm = getattr(module, obj_name)
if self._is_correct_class(itm):
classes.append(itm)
return classes
def get_all_classes(self):
"""Get all classes
Get the classes of the type we want from all modules found
in the directory that defines this class.
"""
classes = []
for dirpath, dirnames, filenames in os.walk(self.path):
relpath = os.path.relpath(dirpath, self.path)
if relpath == '.':
relpkg = ''
else:
relpkg = '.%s' % '.'.join(relpath.split(os.sep))
for fname in filenames:
root, ext = os.path.splitext(fname)
if ext != '.py' or root == '__init__':
continue
module_name = "%s%s.%s" % (self.package, relpkg, root)
mod_classes = self._get_classes_from_module(module_name)
classes.extend(mod_classes)
return classes
def get_matching_classes(self, loadable_class_names):
"""Get loadable classes from a list of names.
Each name can be a full module path or the full path to a
method that returns classes to use. The latter behavior
is useful to specify a method that returns a list of
classes to use in a default case.
"""
classes = []
for cls_name in loadable_class_names:
obj = importutils.import_class(cls_name)
if self._is_correct_class(obj):
classes.append(obj)
elif inspect.isfunction(obj):
# Get list of classes from a function
for cls in obj():
classes.append(cls)
else:
error_str = 'Not a class of the correct type'
raise exception.ClassNotFound(class_name=cls_name,
exception=error_str)
return classes