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:
parent
92387ce610
commit
dd63b06a4c
@ -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
|
||||
|
@ -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")
|
||||
|
@ -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.
|
||||
"""),
|
||||
]
|
||||
|
||||
|
111
zun/scheduler/base_filters.py
Normal file
111
zun/scheduler/base_filters.py
Normal 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
|
99
zun/scheduler/filter_scheduler.py
Normal file
99
zun/scheduler/filter_scheduler.py
Normal 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
|
45
zun/scheduler/filters/__init__.py
Normal file
45
zun/scheduler/filters/__init__.py
Normal 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()
|
38
zun/scheduler/filters/noop_filter.py
Normal file
38
zun/scheduler/filters/noop_filter.py
Normal 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
121
zun/scheduler/loadables.py
Normal 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
|
Loading…
x
Reference in New Issue
Block a user