scheduler: Merge 'FilterScheduler' into base class
There are no longer any custom filters. We don't need the abstract base class. Merge the code in and give it a more useful 'SchedulerDriver' name. Change-Id: Id08dafa72d617ca85e66d50b3c91045e0e8723d0 Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
This commit is contained in:
parent
6d745036bc
commit
e0534cc289
@ -33,7 +33,7 @@ better to begin the process with a candidate task or method *within* the
|
|||||||
service that can be associated with an identifier. For example,
|
service that can be associated with an identifier. For example,
|
||||||
``select_destinations`` in the ``FilterScheduler`` can be associated with the
|
``select_destinations`` in the ``FilterScheduler`` can be associated with the
|
||||||
list of ``instance_uuids`` passed to it and it runs only once for that set of
|
list of ``instance_uuids`` passed to it and it runs only once for that set of
|
||||||
instance uuids.
|
instance UUIDs.
|
||||||
|
|
||||||
The process for profiling is:
|
The process for profiling is:
|
||||||
|
|
||||||
@ -100,52 +100,19 @@ profiling and benchmarking scenarios so not all changes are relevant here):
|
|||||||
[notifications]
|
[notifications]
|
||||||
notification_format = unversioned
|
notification_format = unversioned
|
||||||
|
|
||||||
Change the code in ``nova/scheduler/filter_scheduler.py`` as follows:
|
Change the code in ``nova/scheduler/driver.py`` as follows to start the
|
||||||
|
profiler at the start of ``select_destinations`` call and to dump the
|
||||||
|
statistics at the end. For example:
|
||||||
|
|
||||||
.. code-block:: diff
|
.. code-block:: diff
|
||||||
|
|
||||||
|
diff --git nova/scheduler/driver.py nova/scheduler/driver.py
|
||||||
diff --git a/nova/scheduler/filter_scheduler.py b/nova/scheduler/filter_scheduler.py
|
index 555236e8a1..efa84b5a47 100644
|
||||||
index 672f23077e..cb0f87fe48 100644
|
--- nova/scheduler/driver.py
|
||||||
--- a/nova/scheduler/filter_scheduler.py
|
+++ nova/scheduler/driver.py
|
||||||
+++ b/nova/scheduler/filter_scheduler.py
|
@@ -95,6 +95,10 @@ class SchedulerDriver:
|
||||||
@@ -49,92 +49,99 @@ class FilterScheduler(driver.Scheduler):
|
determined by the configuration option
|
||||||
def select_destinations(self, context, spec_obj, instance_uuids,
|
`CONF.scheduler.max_attempts`.
|
||||||
alloc_reqs_by_rp_uuid, provider_summaries,
|
|
||||||
allocation_request_version=None, return_alternates=False):
|
|
||||||
"""Returns a list of lists of Selection objects, which represent the
|
|
||||||
hosts and (optionally) alternates for each instance.
|
|
||||||
|
|
||||||
:param context: The RequestContext object
|
|
||||||
:param spec_obj: The RequestSpec object
|
|
||||||
:param instance_uuids: List of UUIDs, one for each value of the spec
|
|
||||||
object's num_instances attribute
|
|
||||||
:param alloc_reqs_by_rp_uuid: Optional dict, keyed by resource provider
|
|
||||||
UUID, of the allocation_requests that may
|
|
||||||
be used to claim resources against
|
|
||||||
matched hosts. If None, indicates either
|
|
||||||
the placement API wasn't reachable or
|
|
||||||
that there were no allocation_requests
|
|
||||||
returned by the placement API. If the
|
|
||||||
latter, the provider_summaries will be an
|
|
||||||
empty dict, not None.
|
|
||||||
:param provider_summaries: Optional dict, keyed by resource provider
|
|
||||||
UUID, of information that will be used by
|
|
||||||
the filters/weighers in selecting matching
|
|
||||||
hosts for a request. If None, indicates that
|
|
||||||
the scheduler driver should grab all compute
|
|
||||||
node information locally and that the
|
|
||||||
Placement API is not used. If an empty dict,
|
|
||||||
indicates the Placement API returned no
|
|
||||||
potential matches for the requested
|
|
||||||
resources.
|
|
||||||
:param allocation_request_version: The microversion used to request the
|
|
||||||
allocations.
|
|
||||||
:param return_alternates: When True, zero or more alternate hosts are
|
|
||||||
returned with each selected host. The number
|
|
||||||
of alternates is determined by the
|
|
||||||
configuration option
|
|
||||||
`CONF.scheduler.max_attempts`.
|
|
||||||
"""
|
"""
|
||||||
+ from eventlet.green import profile
|
+ from eventlet.green import profile
|
||||||
+ pr = profile.Profile()
|
+ pr = profile.Profile()
|
||||||
@ -153,27 +120,19 @@ Change the code in ``nova/scheduler/filter_scheduler.py`` as follows:
|
|||||||
+
|
+
|
||||||
self.notifier.info(
|
self.notifier.info(
|
||||||
context, 'scheduler.select_destinations.start',
|
context, 'scheduler.select_destinations.start',
|
||||||
dict(request_spec=spec_obj.to_legacy_request_spec_dict()))
|
{'request_spec': spec_obj.to_legacy_request_spec_dict()})
|
||||||
compute_utils.notify_about_scheduler_action(
|
@@ -114,6 +118,10 @@ class SchedulerDriver:
|
||||||
context=context, request_spec=spec_obj,
|
|
||||||
action=fields_obj.NotificationAction.SELECT_DESTINATIONS,
|
|
||||||
phase=fields_obj.NotificationPhase.START)
|
|
||||||
|
|
||||||
host_selections = self._schedule(context, spec_obj, instance_uuids,
|
|
||||||
alloc_reqs_by_rp_uuid, provider_summaries,
|
|
||||||
allocation_request_version, return_alternates)
|
|
||||||
self.notifier.info(
|
|
||||||
context, 'scheduler.select_destinations.end',
|
|
||||||
dict(request_spec=spec_obj.to_legacy_request_spec_dict()))
|
|
||||||
compute_utils.notify_about_scheduler_action(
|
|
||||||
context=context, request_spec=spec_obj,
|
context=context, request_spec=spec_obj,
|
||||||
action=fields_obj.NotificationAction.SELECT_DESTINATIONS,
|
action=fields_obj.NotificationAction.SELECT_DESTINATIONS,
|
||||||
phase=fields_obj.NotificationPhase.END)
|
phase=fields_obj.NotificationPhase.END)
|
||||||
|
+
|
||||||
+ pr.stop()
|
+ pr.stop()
|
||||||
+ pr.dump_stats('/tmp/select_destinations/%s.prof' % ':'.join(instance_uuids))
|
+ pr.dump_stats('/tmp/select_destinations/%s.prof' % ':'.join(instance_uuids))
|
||||||
+
|
+
|
||||||
return host_selections
|
return host_selections
|
||||||
|
|
||||||
|
def _schedule(
|
||||||
|
|
||||||
Make a ``/tmp/select_destinations`` directory that is writable by the user
|
Make a ``/tmp/select_destinations`` directory that is writable by the user
|
||||||
nova-scheduler will run as. This is where the profile output will go.
|
nova-scheduler will run as. This is where the profile output will go.
|
||||||
|
|
||||||
@ -189,7 +148,7 @@ Create a server (which will call ``select_destinations``)::
|
|||||||
openstack server create --image cirros-0.4.0-x86_64-disk --flavor c1 x1
|
openstack server create --image cirros-0.4.0-x86_64-disk --flavor c1 x1
|
||||||
|
|
||||||
In ``/tmp/select_destinations`` there should be a file with a name using the
|
In ``/tmp/select_destinations`` there should be a file with a name using the
|
||||||
uuid of the created server with a ``.prof`` extension.
|
UUID of the created server with a ``.prof`` extension.
|
||||||
|
|
||||||
Change to that directory and view the profile using the pstats
|
Change to that directory and view the profile using the pstats
|
||||||
`interactive mode`_::
|
`interactive mode`_::
|
||||||
|
@ -60,10 +60,12 @@ def replace_allocation_with_migration(context, instance, migration):
|
|||||||
context, instance.uuid)['allocations']
|
context, instance.uuid)['allocations']
|
||||||
root_alloc = orig_alloc.get(source_cn.uuid, {}).get('resources', {})
|
root_alloc = orig_alloc.get(source_cn.uuid, {}).get('resources', {})
|
||||||
if not root_alloc:
|
if not root_alloc:
|
||||||
LOG.debug('Unable to find existing allocations for instance on '
|
# TODO(stephenfin): This was a valid code path when there was support
|
||||||
'source compute node: %s. This is normal if you are not '
|
# for multiple schedulers, but it should probably be an error now
|
||||||
'using the FilterScheduler.', source_cn.uuid,
|
LOG.debug(
|
||||||
instance=instance)
|
'Unable to find existing allocations for instance on '
|
||||||
|
'source compute node: %s',
|
||||||
|
source_cn.uuid, instance=instance)
|
||||||
return None, None
|
return None, None
|
||||||
|
|
||||||
# FIXME(gibi): This method is flawed in that it does not handle allocations
|
# FIXME(gibi): This method is flawed in that it does not handle allocations
|
||||||
|
@ -16,27 +16,490 @@
|
|||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Scheduler base class that all Schedulers should inherit from
|
Driver for the nova-scheduler service.
|
||||||
|
|
||||||
|
You can customize this scheduler by specifying your own host filters and
|
||||||
|
weighers.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import abc
|
import random
|
||||||
|
|
||||||
|
from oslo_log import log as logging
|
||||||
|
|
||||||
|
from nova.compute import utils as compute_utils
|
||||||
|
import nova.conf
|
||||||
|
from nova import exception
|
||||||
|
from nova.i18n import _
|
||||||
|
from nova import objects
|
||||||
|
from nova.objects import fields as fields_obj
|
||||||
|
from nova import rpc
|
||||||
|
from nova.scheduler.client import report
|
||||||
from nova.scheduler import host_manager
|
from nova.scheduler import host_manager
|
||||||
|
from nova.scheduler import utils
|
||||||
from nova import servicegroup
|
from nova import servicegroup
|
||||||
|
|
||||||
|
CONF = nova.conf.CONF
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
class Scheduler(metaclass=abc.ABCMeta):
|
|
||||||
"""The base class that all Scheduler classes should inherit from."""
|
class SchedulerDriver:
|
||||||
|
"""The scheduler driver.
|
||||||
|
|
||||||
|
Filters and weighs compute hosts to determine the best host to schedule an
|
||||||
|
instance to.
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.host_manager = host_manager.HostManager()
|
self.host_manager = host_manager.HostManager()
|
||||||
self.servicegroup_api = servicegroup.API()
|
self.servicegroup_api = servicegroup.API()
|
||||||
|
self.notifier = rpc.get_notifier('scheduler')
|
||||||
|
self.placement_client = report.SchedulerReportClient()
|
||||||
|
|
||||||
@abc.abstractmethod
|
def select_destinations(
|
||||||
def select_destinations(self, context, spec_obj, instance_uuids,
|
self, context, spec_obj, instance_uuids,
|
||||||
alloc_reqs_by_rp_uuid, provider_summaries,
|
alloc_reqs_by_rp_uuid, provider_summaries,
|
||||||
allocation_request_version=None, return_alternates=False):
|
allocation_request_version=None, return_alternates=False,
|
||||||
|
):
|
||||||
"""Returns a list of lists of Selection objects that have been chosen
|
"""Returns a list of lists of Selection objects that have been chosen
|
||||||
by the scheduler driver, one for each requested instance.
|
by the scheduler driver, one for each requested instance.
|
||||||
|
|
||||||
|
:param context: The RequestContext object
|
||||||
|
:param spec_obj: The RequestSpec object
|
||||||
|
:param instance_uuids: List of UUIDs, one for each value of the spec
|
||||||
|
object's num_instances attribute
|
||||||
|
:param alloc_reqs_by_rp_uuid: Optional dict, keyed by resource provider
|
||||||
|
UUID, of the allocation_requests that may be used to claim
|
||||||
|
resources against matched hosts. If None, indicates either the
|
||||||
|
placement API wasn't reachable or that there were no
|
||||||
|
allocation_requests returned by the placement API. If the latter,
|
||||||
|
the provider_summaries will be an empty dict, not None.
|
||||||
|
:param provider_summaries: Optional dict, keyed by resource provider
|
||||||
|
UUID, of information that will be used by the filters/weighers in
|
||||||
|
selecting matching hosts for a request. If None, indicates that the
|
||||||
|
scheduler driver should grab all compute node information locally
|
||||||
|
and that the Placement API is not used. If an empty dict, indicates
|
||||||
|
the Placement API returned no potential matches for the requested
|
||||||
|
resources.
|
||||||
|
:param allocation_request_version: The microversion used to request the
|
||||||
|
allocations.
|
||||||
|
:param return_alternates: When True, zero or more alternate hosts are
|
||||||
|
returned with each selected host. The number of alternates is
|
||||||
|
determined by the configuration option
|
||||||
|
`CONF.scheduler.max_attempts`.
|
||||||
"""
|
"""
|
||||||
return []
|
self.notifier.info(
|
||||||
|
context, 'scheduler.select_destinations.start',
|
||||||
|
{'request_spec': spec_obj.to_legacy_request_spec_dict()})
|
||||||
|
compute_utils.notify_about_scheduler_action(
|
||||||
|
context=context, request_spec=spec_obj,
|
||||||
|
action=fields_obj.NotificationAction.SELECT_DESTINATIONS,
|
||||||
|
phase=fields_obj.NotificationPhase.START)
|
||||||
|
|
||||||
|
host_selections = self._schedule(
|
||||||
|
context, spec_obj, instance_uuids,
|
||||||
|
alloc_reqs_by_rp_uuid, provider_summaries,
|
||||||
|
allocation_request_version, return_alternates)
|
||||||
|
self.notifier.info(
|
||||||
|
context, 'scheduler.select_destinations.end',
|
||||||
|
{'request_spec': spec_obj.to_legacy_request_spec_dict()})
|
||||||
|
compute_utils.notify_about_scheduler_action(
|
||||||
|
context=context, request_spec=spec_obj,
|
||||||
|
action=fields_obj.NotificationAction.SELECT_DESTINATIONS,
|
||||||
|
phase=fields_obj.NotificationPhase.END)
|
||||||
|
return host_selections
|
||||||
|
|
||||||
|
def _schedule(
|
||||||
|
self, context, spec_obj, instance_uuids, alloc_reqs_by_rp_uuid,
|
||||||
|
provider_summaries, allocation_request_version=None,
|
||||||
|
return_alternates=False
|
||||||
|
):
|
||||||
|
"""Returns a list of lists of Selection objects.
|
||||||
|
|
||||||
|
:param context: The RequestContext object
|
||||||
|
:param spec_obj: The RequestSpec object
|
||||||
|
:param instance_uuids: List of instance UUIDs to place or move.
|
||||||
|
:param alloc_reqs_by_rp_uuid: Optional dict, keyed by resource provider
|
||||||
|
UUID, of the allocation_requests that may be used to claim
|
||||||
|
resources against matched hosts. If None, indicates either the
|
||||||
|
placement API wasn't reachable or that there were no
|
||||||
|
allocation_requests returned by the placement API. If the latter,
|
||||||
|
the provider_summaries will be an empty dict, not None.
|
||||||
|
:param provider_summaries: Optional dict, keyed by resource provider
|
||||||
|
UUID, of information that will be used by the filters/weighers in
|
||||||
|
selecting matching hosts for a request. If None, indicates that the
|
||||||
|
scheduler driver should grab all compute node information locally
|
||||||
|
and that the Placement API is not used. If an empty dict, indicates
|
||||||
|
the Placement API returned no potential matches for the requested
|
||||||
|
resources.
|
||||||
|
:param allocation_request_version: The microversion used to request the
|
||||||
|
allocations.
|
||||||
|
:param return_alternates: When True, zero or more alternate hosts are
|
||||||
|
returned with each selected host. The number of alternates is
|
||||||
|
determined by the configuration option
|
||||||
|
`CONF.scheduler.max_attempts`.
|
||||||
|
"""
|
||||||
|
elevated = context.elevated()
|
||||||
|
|
||||||
|
# Find our local list of acceptable hosts by repeatedly
|
||||||
|
# filtering and weighing our options. Each time we choose a
|
||||||
|
# host, we virtually consume resources on it so subsequent
|
||||||
|
# selections can adjust accordingly.
|
||||||
|
|
||||||
|
# Note: remember, we are using a generator-iterator here. So only
|
||||||
|
# traverse this list once. This can bite you if the hosts
|
||||||
|
# are being scanned in a filter or weighing function.
|
||||||
|
hosts = self._get_all_host_states(
|
||||||
|
elevated, spec_obj, provider_summaries)
|
||||||
|
|
||||||
|
# NOTE(sbauza): The RequestSpec.num_instances field contains the number
|
||||||
|
# of instances created when the RequestSpec was used to first boot some
|
||||||
|
# instances. This is incorrect when doing a move or resize operation,
|
||||||
|
# so prefer the length of instance_uuids unless it is None.
|
||||||
|
num_instances = (len(instance_uuids) if instance_uuids
|
||||||
|
else spec_obj.num_instances)
|
||||||
|
|
||||||
|
# For each requested instance, we want to return a host whose resources
|
||||||
|
# for the instance have been claimed, along with zero or more
|
||||||
|
# alternates. These alternates will be passed to the cell that the
|
||||||
|
# selected host is in, so that if for some reason the build fails, the
|
||||||
|
# cell conductor can retry building the instance on one of these
|
||||||
|
# alternates instead of having to simply fail. The number of alternates
|
||||||
|
# is based on CONF.scheduler.max_attempts; note that if there are not
|
||||||
|
# enough filtered hosts to provide the full number of alternates, the
|
||||||
|
# list of hosts may be shorter than this amount.
|
||||||
|
num_alts = CONF.scheduler.max_attempts - 1 if return_alternates else 0
|
||||||
|
|
||||||
|
if instance_uuids is None or alloc_reqs_by_rp_uuid is None:
|
||||||
|
# If there was a problem communicating with the
|
||||||
|
# placement API, alloc_reqs_by_rp_uuid will be None, so we skip
|
||||||
|
# claiming in that case as well. In the case where instance_uuids
|
||||||
|
# is None, that indicates an older conductor, so we need to return
|
||||||
|
# the objects without alternates. They will be converted back to
|
||||||
|
# the older dict format representing HostState objects.
|
||||||
|
# TODO(stephenfin): Remove this when we bump scheduler the RPC API
|
||||||
|
# version to 5.0
|
||||||
|
return self._legacy_find_hosts(
|
||||||
|
context, num_instances, spec_obj, hosts, num_alts,
|
||||||
|
instance_uuids=instance_uuids)
|
||||||
|
|
||||||
|
# A list of the instance UUIDs that were successfully claimed against
|
||||||
|
# in the placement API. If we are not able to successfully claim for
|
||||||
|
# all involved instances, we use this list to remove those allocations
|
||||||
|
# before returning
|
||||||
|
claimed_instance_uuids = []
|
||||||
|
|
||||||
|
# The list of hosts that have been selected (and claimed).
|
||||||
|
claimed_hosts = []
|
||||||
|
|
||||||
|
for num, instance_uuid in enumerate(instance_uuids):
|
||||||
|
# In a multi-create request, the first request spec from the list
|
||||||
|
# is passed to the scheduler and that request spec's instance_uuid
|
||||||
|
# might not be the same as the instance we're processing, so we
|
||||||
|
# update the instance_uuid in that case before passing the request
|
||||||
|
# spec to filters since at least one filter
|
||||||
|
# (ServerGroupAntiAffinityFilter) depends on that information being
|
||||||
|
# accurate.
|
||||||
|
spec_obj.instance_uuid = instance_uuid
|
||||||
|
# Reset the field so it's not persisted accidentally.
|
||||||
|
spec_obj.obj_reset_changes(['instance_uuid'])
|
||||||
|
|
||||||
|
hosts = self._get_sorted_hosts(spec_obj, hosts, num)
|
||||||
|
if not hosts:
|
||||||
|
# NOTE(jaypipes): If we get here, that means not all instances
|
||||||
|
# in instance_uuids were able to be matched to a selected host.
|
||||||
|
# Any allocations will be cleaned up in the
|
||||||
|
# _ensure_sufficient_hosts() call.
|
||||||
|
break
|
||||||
|
|
||||||
|
# Attempt to claim the resources against one or more resource
|
||||||
|
# providers, looping over the sorted list of possible hosts
|
||||||
|
# looking for an allocation_request that contains that host's
|
||||||
|
# resource provider UUID
|
||||||
|
claimed_host = None
|
||||||
|
for host in hosts:
|
||||||
|
cn_uuid = host.uuid
|
||||||
|
if cn_uuid not in alloc_reqs_by_rp_uuid:
|
||||||
|
msg = ("A host state with uuid = '%s' that did not have a "
|
||||||
|
"matching allocation_request was encountered while "
|
||||||
|
"scheduling. This host was skipped.")
|
||||||
|
LOG.debug(msg, cn_uuid)
|
||||||
|
continue
|
||||||
|
|
||||||
|
alloc_reqs = alloc_reqs_by_rp_uuid[cn_uuid]
|
||||||
|
# TODO(jaypipes): Loop through all allocation_requests instead
|
||||||
|
# of just trying the first one. For now, since we'll likely
|
||||||
|
# want to order the allocation_requests in the future based on
|
||||||
|
# information in the provider summaries, we'll just try to
|
||||||
|
# claim resources using the first allocation_request
|
||||||
|
alloc_req = alloc_reqs[0]
|
||||||
|
if utils.claim_resources(
|
||||||
|
elevated, self.placement_client, spec_obj, instance_uuid,
|
||||||
|
alloc_req,
|
||||||
|
allocation_request_version=allocation_request_version,
|
||||||
|
):
|
||||||
|
claimed_host = host
|
||||||
|
break
|
||||||
|
|
||||||
|
if claimed_host is None:
|
||||||
|
# We weren't able to claim resources in the placement API
|
||||||
|
# for any of the sorted hosts identified. So, clean up any
|
||||||
|
# successfully-claimed resources for prior instances in
|
||||||
|
# this request and return an empty list which will cause
|
||||||
|
# select_destinations() to raise NoValidHost
|
||||||
|
LOG.debug("Unable to successfully claim against any host.")
|
||||||
|
break
|
||||||
|
|
||||||
|
claimed_instance_uuids.append(instance_uuid)
|
||||||
|
claimed_hosts.append(claimed_host)
|
||||||
|
|
||||||
|
# Now consume the resources so the filter/weights will change for
|
||||||
|
# the next instance.
|
||||||
|
self._consume_selected_host(
|
||||||
|
claimed_host, spec_obj, instance_uuid=instance_uuid)
|
||||||
|
|
||||||
|
# Check if we were able to fulfill the request. If not, this call will
|
||||||
|
# raise a NoValidHost exception.
|
||||||
|
self._ensure_sufficient_hosts(
|
||||||
|
context, claimed_hosts, num_instances, claimed_instance_uuids)
|
||||||
|
|
||||||
|
# We have selected and claimed hosts for each instance. Now we need to
|
||||||
|
# find alternates for each host.
|
||||||
|
return self._get_alternate_hosts(
|
||||||
|
claimed_hosts, spec_obj, hosts, num, num_alts,
|
||||||
|
alloc_reqs_by_rp_uuid, allocation_request_version)
|
||||||
|
|
||||||
|
def _ensure_sufficient_hosts(
|
||||||
|
self, context, hosts, required_count, claimed_uuids=None,
|
||||||
|
):
|
||||||
|
"""Checks that we have selected a host for each requested instance. If
|
||||||
|
not, log this failure, remove allocations for any claimed instances,
|
||||||
|
and raise a NoValidHost exception.
|
||||||
|
"""
|
||||||
|
if len(hosts) == required_count:
|
||||||
|
# We have enough hosts.
|
||||||
|
return
|
||||||
|
|
||||||
|
if claimed_uuids:
|
||||||
|
self._cleanup_allocations(context, claimed_uuids)
|
||||||
|
|
||||||
|
# NOTE(Rui Chen): If multiple creates failed, set the updated time
|
||||||
|
# of selected HostState to None so that these HostStates are
|
||||||
|
# refreshed according to database in next schedule, and release
|
||||||
|
# the resource consumed by instance in the process of selecting
|
||||||
|
# host.
|
||||||
|
for host in hosts:
|
||||||
|
host.updated = None
|
||||||
|
|
||||||
|
# Log the details but don't put those into the reason since
|
||||||
|
# we don't want to give away too much information about our
|
||||||
|
# actual environment.
|
||||||
|
LOG.debug(
|
||||||
|
'There are %(hosts)d hosts available but '
|
||||||
|
'%(required_count)d instances requested to build.',
|
||||||
|
{'hosts': len(hosts), 'required_count': required_count})
|
||||||
|
reason = _('There are not enough hosts available.')
|
||||||
|
raise exception.NoValidHost(reason=reason)
|
||||||
|
|
||||||
|
def _cleanup_allocations(self, context, instance_uuids):
|
||||||
|
"""Removes allocations for the supplied instance UUIDs."""
|
||||||
|
if not instance_uuids:
|
||||||
|
return
|
||||||
|
|
||||||
|
LOG.debug("Cleaning up allocations for %s", instance_uuids)
|
||||||
|
for uuid in instance_uuids:
|
||||||
|
self.placement_client.delete_allocation_for_instance(context, uuid)
|
||||||
|
|
||||||
|
def _legacy_find_hosts(
|
||||||
|
self, context, num_instances, spec_obj, hosts, num_alts,
|
||||||
|
instance_uuids=None,
|
||||||
|
):
|
||||||
|
"""Find hosts without invoking placement.
|
||||||
|
|
||||||
|
We may not be able to claim if the Placement service is not reachable.
|
||||||
|
Additionally, we may be working with older conductors that don't pass
|
||||||
|
in instance_uuids.
|
||||||
|
"""
|
||||||
|
# The list of hosts selected for each instance
|
||||||
|
selected_hosts = []
|
||||||
|
|
||||||
|
for num in range(num_instances):
|
||||||
|
instance_uuid = instance_uuids[num] if instance_uuids else None
|
||||||
|
if instance_uuid:
|
||||||
|
# Update the RequestSpec.instance_uuid before sending it to
|
||||||
|
# the filters in case we're doing a multi-create request, but
|
||||||
|
# don't persist the change.
|
||||||
|
spec_obj.instance_uuid = instance_uuid
|
||||||
|
spec_obj.obj_reset_changes(['instance_uuid'])
|
||||||
|
|
||||||
|
hosts = self._get_sorted_hosts(spec_obj, hosts, num)
|
||||||
|
if not hosts:
|
||||||
|
# No hosts left, so break here, and the
|
||||||
|
# _ensure_sufficient_hosts() call below will handle this.
|
||||||
|
break
|
||||||
|
|
||||||
|
selected_host = hosts[0]
|
||||||
|
selected_hosts.append(selected_host)
|
||||||
|
self._consume_selected_host(
|
||||||
|
selected_host, spec_obj, instance_uuid=instance_uuid)
|
||||||
|
|
||||||
|
# Check if we were able to fulfill the request. If not, this call will
|
||||||
|
# raise a NoValidHost exception.
|
||||||
|
self._ensure_sufficient_hosts(context, selected_hosts, num_instances)
|
||||||
|
|
||||||
|
# This the overall list of values to be returned. There will be one
|
||||||
|
# item per instance, and each item will be a list of Selection objects
|
||||||
|
# representing the selected host along with zero or more alternates
|
||||||
|
# from the same cell.
|
||||||
|
return self._get_alternate_hosts(
|
||||||
|
selected_hosts, spec_obj, hosts, num, num_alts)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _consume_selected_host(selected_host, spec_obj, instance_uuid=None):
|
||||||
|
LOG.debug(
|
||||||
|
"Selected host: %(host)s", {'host': selected_host},
|
||||||
|
instance_uuid=instance_uuid)
|
||||||
|
selected_host.consume_from_request(spec_obj)
|
||||||
|
# If we have a server group, add the selected host to it for the
|
||||||
|
# (anti-)affinity filters to filter out hosts for subsequent instances
|
||||||
|
# in a multi-create request.
|
||||||
|
if spec_obj.instance_group is not None:
|
||||||
|
spec_obj.instance_group.hosts.append(selected_host.host)
|
||||||
|
# hosts has to be not part of the updates when saving
|
||||||
|
spec_obj.instance_group.obj_reset_changes(['hosts'])
|
||||||
|
# The ServerGroupAntiAffinityFilter also relies on
|
||||||
|
# HostState.instances being accurate within a multi-create request.
|
||||||
|
if instance_uuid and instance_uuid not in selected_host.instances:
|
||||||
|
# Set a stub since ServerGroupAntiAffinityFilter only cares
|
||||||
|
# about the keys.
|
||||||
|
selected_host.instances[instance_uuid] = objects.Instance(
|
||||||
|
uuid=instance_uuid)
|
||||||
|
|
||||||
|
def _get_alternate_hosts(
|
||||||
|
self, selected_hosts, spec_obj, hosts, index, num_alts,
|
||||||
|
alloc_reqs_by_rp_uuid=None, allocation_request_version=None,
|
||||||
|
):
|
||||||
|
# We only need to filter/weigh the hosts again if we're dealing with
|
||||||
|
# more than one instance and are going to be picking alternates.
|
||||||
|
if index > 0 and num_alts > 0:
|
||||||
|
# The selected_hosts have all had resources 'claimed' via
|
||||||
|
# _consume_selected_host, so we need to filter/weigh and sort the
|
||||||
|
# hosts again to get an accurate count for alternates.
|
||||||
|
hosts = self._get_sorted_hosts(spec_obj, hosts, index)
|
||||||
|
|
||||||
|
# This is the overall list of values to be returned. There will be one
|
||||||
|
# item per instance, and each item will be a list of Selection objects
|
||||||
|
# representing the selected host along with alternates from the same
|
||||||
|
# cell.
|
||||||
|
selections_to_return = []
|
||||||
|
for selected_host in selected_hosts:
|
||||||
|
# This is the list of hosts for one particular instance.
|
||||||
|
if alloc_reqs_by_rp_uuid:
|
||||||
|
selected_alloc_req = alloc_reqs_by_rp_uuid.get(
|
||||||
|
selected_host.uuid)[0]
|
||||||
|
else:
|
||||||
|
selected_alloc_req = None
|
||||||
|
|
||||||
|
selection = objects.Selection.from_host_state(
|
||||||
|
selected_host, allocation_request=selected_alloc_req,
|
||||||
|
allocation_request_version=allocation_request_version)
|
||||||
|
selected_plus_alts = [selection]
|
||||||
|
cell_uuid = selected_host.cell_uuid
|
||||||
|
|
||||||
|
# This will populate the alternates with many of the same unclaimed
|
||||||
|
# hosts. This is OK, as it should be rare for a build to fail. And
|
||||||
|
# if there are not enough hosts to fully populate the alternates,
|
||||||
|
# it's fine to return fewer than we'd like. Note that we exclude
|
||||||
|
# any claimed host from consideration as an alternate because it
|
||||||
|
# will have had its resources reduced and will have a much lower
|
||||||
|
# chance of being able to fit another instance on it.
|
||||||
|
for host in hosts:
|
||||||
|
if len(selected_plus_alts) >= num_alts + 1:
|
||||||
|
break
|
||||||
|
|
||||||
|
if host.cell_uuid == cell_uuid and host not in selected_hosts:
|
||||||
|
if alloc_reqs_by_rp_uuid is not None:
|
||||||
|
alt_uuid = host.uuid
|
||||||
|
if alt_uuid not in alloc_reqs_by_rp_uuid:
|
||||||
|
msg = ("A host state with uuid = '%s' that did "
|
||||||
|
"not have a matching allocation_request "
|
||||||
|
"was encountered while scheduling. This "
|
||||||
|
"host was skipped.")
|
||||||
|
LOG.debug(msg, alt_uuid)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# TODO(jaypipes): Loop through all allocation_requests
|
||||||
|
# instead of just trying the first one. For now, since
|
||||||
|
# we'll likely want to order the allocation_requests in
|
||||||
|
# the future based on information in the provider
|
||||||
|
# summaries, we'll just try to claim resources using
|
||||||
|
# the first allocation_request
|
||||||
|
alloc_req = alloc_reqs_by_rp_uuid[alt_uuid][0]
|
||||||
|
alt_selection = objects.Selection.from_host_state(
|
||||||
|
host, alloc_req, allocation_request_version)
|
||||||
|
else:
|
||||||
|
alt_selection = objects.Selection.from_host_state(host)
|
||||||
|
selected_plus_alts.append(alt_selection)
|
||||||
|
|
||||||
|
selections_to_return.append(selected_plus_alts)
|
||||||
|
|
||||||
|
return selections_to_return
|
||||||
|
|
||||||
|
def _get_sorted_hosts(self, spec_obj, host_states, index):
|
||||||
|
"""Returns a list of HostState objects that match the required
|
||||||
|
scheduling constraints for the request spec object and have been sorted
|
||||||
|
according to the weighers.
|
||||||
|
"""
|
||||||
|
filtered_hosts = self.host_manager.get_filtered_hosts(host_states,
|
||||||
|
spec_obj, index)
|
||||||
|
|
||||||
|
LOG.debug("Filtered %(hosts)s", {'hosts': filtered_hosts})
|
||||||
|
|
||||||
|
if not filtered_hosts:
|
||||||
|
return []
|
||||||
|
|
||||||
|
weighed_hosts = self.host_manager.get_weighed_hosts(
|
||||||
|
filtered_hosts, spec_obj)
|
||||||
|
if CONF.filter_scheduler.shuffle_best_same_weighed_hosts:
|
||||||
|
# NOTE(pas-ha) Randomize best hosts, relying on weighed_hosts
|
||||||
|
# being already sorted by weight in descending order.
|
||||||
|
# This decreases possible contention and rescheduling attempts
|
||||||
|
# when there is a large number of hosts having the same best
|
||||||
|
# weight, especially so when host_subset_size is 1 (default)
|
||||||
|
best_hosts = [
|
||||||
|
w for w in weighed_hosts
|
||||||
|
if w.weight == weighed_hosts[0].weight
|
||||||
|
]
|
||||||
|
random.shuffle(best_hosts)
|
||||||
|
weighed_hosts = best_hosts + weighed_hosts[len(best_hosts):]
|
||||||
|
|
||||||
|
# Log the weighed hosts before stripping off the wrapper class so that
|
||||||
|
# the weight value gets logged.
|
||||||
|
LOG.debug("Weighed %(hosts)s", {'hosts': weighed_hosts})
|
||||||
|
# Strip off the WeighedHost wrapper class...
|
||||||
|
weighed_hosts = [h.obj for h in weighed_hosts]
|
||||||
|
|
||||||
|
# We randomize the first element in the returned list to alleviate
|
||||||
|
# congestion where the same host is consistently selected among
|
||||||
|
# numerous potential hosts for similar request specs.
|
||||||
|
host_subset_size = CONF.filter_scheduler.host_subset_size
|
||||||
|
if host_subset_size < len(weighed_hosts):
|
||||||
|
weighed_subset = weighed_hosts[0:host_subset_size]
|
||||||
|
else:
|
||||||
|
weighed_subset = weighed_hosts
|
||||||
|
|
||||||
|
chosen_host = random.choice(weighed_subset)
|
||||||
|
weighed_hosts.remove(chosen_host)
|
||||||
|
return [chosen_host] + weighed_hosts
|
||||||
|
|
||||||
|
def _get_all_host_states(self, context, spec_obj, provider_summaries):
|
||||||
|
"""Template method, so a subclass can implement caching."""
|
||||||
|
# The provider_summaries variable will be an empty dict when the
|
||||||
|
# Placement API found no providers that match the requested
|
||||||
|
# constraints, which in turn makes compute_uuids an empty list and
|
||||||
|
# get_host_states_by_uuids will return an empty generator-iterator
|
||||||
|
# also, which will eventually result in a NoValidHost error.
|
||||||
|
compute_uuids = None
|
||||||
|
if provider_summaries is not None:
|
||||||
|
compute_uuids = list(provider_summaries.keys())
|
||||||
|
return self.host_manager.get_host_states_by_uuids(
|
||||||
|
context, compute_uuids, spec_obj)
|
||||||
|
@ -1,489 +0,0 @@
|
|||||||
# Copyright (c) 2011 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.
|
|
||||||
|
|
||||||
"""
|
|
||||||
The FilterScheduler is for creating instances locally.
|
|
||||||
You can customize this scheduler by specifying your own Host Filters and
|
|
||||||
Weighing Functions.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import random
|
|
||||||
|
|
||||||
from oslo_log import log as logging
|
|
||||||
|
|
||||||
from nova.compute import utils as compute_utils
|
|
||||||
import nova.conf
|
|
||||||
from nova import exception
|
|
||||||
from nova.i18n import _
|
|
||||||
from nova import objects
|
|
||||||
from nova.objects import fields as fields_obj
|
|
||||||
from nova import rpc
|
|
||||||
from nova.scheduler.client import report
|
|
||||||
from nova.scheduler import driver
|
|
||||||
from nova.scheduler import utils
|
|
||||||
|
|
||||||
CONF = nova.conf.CONF
|
|
||||||
LOG = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class FilterScheduler(driver.Scheduler):
|
|
||||||
"""Scheduler that can be used for filtering and weighing."""
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super(FilterScheduler, self).__init__(*args, **kwargs)
|
|
||||||
self.notifier = rpc.get_notifier('scheduler')
|
|
||||||
self.placement_client = report.SchedulerReportClient()
|
|
||||||
|
|
||||||
def select_destinations(self, context, spec_obj, instance_uuids,
|
|
||||||
alloc_reqs_by_rp_uuid, provider_summaries,
|
|
||||||
allocation_request_version=None, return_alternates=False):
|
|
||||||
"""Returns a list of lists of Selection objects, which represent the
|
|
||||||
hosts and (optionally) alternates for each instance.
|
|
||||||
|
|
||||||
:param context: The RequestContext object
|
|
||||||
:param spec_obj: The RequestSpec object
|
|
||||||
:param instance_uuids: List of UUIDs, one for each value of the spec
|
|
||||||
object's num_instances attribute
|
|
||||||
:param alloc_reqs_by_rp_uuid: Optional dict, keyed by resource provider
|
|
||||||
UUID, of the allocation_requests that may
|
|
||||||
be used to claim resources against
|
|
||||||
matched hosts. If None, indicates either
|
|
||||||
the placement API wasn't reachable or
|
|
||||||
that there were no allocation_requests
|
|
||||||
returned by the placement API. If the
|
|
||||||
latter, the provider_summaries will be an
|
|
||||||
empty dict, not None.
|
|
||||||
:param provider_summaries: Optional dict, keyed by resource provider
|
|
||||||
UUID, of information that will be used by
|
|
||||||
the filters/weighers in selecting matching
|
|
||||||
hosts for a request. If None, indicates that
|
|
||||||
the scheduler driver should grab all compute
|
|
||||||
node information locally and that the
|
|
||||||
Placement API is not used. If an empty dict,
|
|
||||||
indicates the Placement API returned no
|
|
||||||
potential matches for the requested
|
|
||||||
resources.
|
|
||||||
:param allocation_request_version: The microversion used to request the
|
|
||||||
allocations.
|
|
||||||
:param return_alternates: When True, zero or more alternate hosts are
|
|
||||||
returned with each selected host. The number
|
|
||||||
of alternates is determined by the
|
|
||||||
configuration option
|
|
||||||
`CONF.scheduler.max_attempts`.
|
|
||||||
"""
|
|
||||||
self.notifier.info(
|
|
||||||
context, 'scheduler.select_destinations.start',
|
|
||||||
dict(request_spec=spec_obj.to_legacy_request_spec_dict()))
|
|
||||||
compute_utils.notify_about_scheduler_action(
|
|
||||||
context=context, request_spec=spec_obj,
|
|
||||||
action=fields_obj.NotificationAction.SELECT_DESTINATIONS,
|
|
||||||
phase=fields_obj.NotificationPhase.START)
|
|
||||||
|
|
||||||
host_selections = self._schedule(context, spec_obj, instance_uuids,
|
|
||||||
alloc_reqs_by_rp_uuid, provider_summaries,
|
|
||||||
allocation_request_version, return_alternates)
|
|
||||||
self.notifier.info(
|
|
||||||
context, 'scheduler.select_destinations.end',
|
|
||||||
dict(request_spec=spec_obj.to_legacy_request_spec_dict()))
|
|
||||||
compute_utils.notify_about_scheduler_action(
|
|
||||||
context=context, request_spec=spec_obj,
|
|
||||||
action=fields_obj.NotificationAction.SELECT_DESTINATIONS,
|
|
||||||
phase=fields_obj.NotificationPhase.END)
|
|
||||||
return host_selections
|
|
||||||
|
|
||||||
def _schedule(self, context, spec_obj, instance_uuids,
|
|
||||||
alloc_reqs_by_rp_uuid, provider_summaries,
|
|
||||||
allocation_request_version=None, return_alternates=False):
|
|
||||||
"""Returns a list of lists of Selection objects.
|
|
||||||
|
|
||||||
:param context: The RequestContext object
|
|
||||||
:param spec_obj: The RequestSpec object
|
|
||||||
:param instance_uuids: List of instance UUIDs to place or move.
|
|
||||||
:param alloc_reqs_by_rp_uuid: Optional dict, keyed by resource provider
|
|
||||||
UUID, of the allocation_requests that may
|
|
||||||
be used to claim resources against
|
|
||||||
matched hosts. If None, indicates either
|
|
||||||
the placement API wasn't reachable or
|
|
||||||
that there were no allocation_requests
|
|
||||||
returned by the placement API. If the
|
|
||||||
latter, the provider_summaries will be an
|
|
||||||
empty dict, not None.
|
|
||||||
:param provider_summaries: Optional dict, keyed by resource provider
|
|
||||||
UUID, of information that will be used by
|
|
||||||
the filters/weighers in selecting matching
|
|
||||||
hosts for a request. If None, indicates that
|
|
||||||
the scheduler driver should grab all compute
|
|
||||||
node information locally and that the
|
|
||||||
Placement API is not used. If an empty dict,
|
|
||||||
indicates the Placement API returned no
|
|
||||||
potential matches for the requested
|
|
||||||
resources.
|
|
||||||
:param allocation_request_version: The microversion used to request the
|
|
||||||
allocations.
|
|
||||||
:param return_alternates: When True, zero or more alternate hosts are
|
|
||||||
returned with each selected host. The number
|
|
||||||
of alternates is determined by the
|
|
||||||
configuration option
|
|
||||||
`CONF.scheduler.max_attempts`.
|
|
||||||
"""
|
|
||||||
elevated = context.elevated()
|
|
||||||
|
|
||||||
# Find our local list of acceptable hosts by repeatedly
|
|
||||||
# filtering and weighing our options. Each time we choose a
|
|
||||||
# host, we virtually consume resources on it so subsequent
|
|
||||||
# selections can adjust accordingly.
|
|
||||||
|
|
||||||
# Note: remember, we are using a generator-iterator here. So only
|
|
||||||
# traverse this list once. This can bite you if the hosts
|
|
||||||
# are being scanned in a filter or weighing function.
|
|
||||||
hosts = self._get_all_host_states(elevated, spec_obj,
|
|
||||||
provider_summaries)
|
|
||||||
|
|
||||||
# NOTE(sbauza): The RequestSpec.num_instances field contains the number
|
|
||||||
# of instances created when the RequestSpec was used to first boot some
|
|
||||||
# instances. This is incorrect when doing a move or resize operation,
|
|
||||||
# so prefer the length of instance_uuids unless it is None.
|
|
||||||
num_instances = (len(instance_uuids) if instance_uuids
|
|
||||||
else spec_obj.num_instances)
|
|
||||||
|
|
||||||
# For each requested instance, we want to return a host whose resources
|
|
||||||
# for the instance have been claimed, along with zero or more
|
|
||||||
# alternates. These alternates will be passed to the cell that the
|
|
||||||
# selected host is in, so that if for some reason the build fails, the
|
|
||||||
# cell conductor can retry building the instance on one of these
|
|
||||||
# alternates instead of having to simply fail. The number of alternates
|
|
||||||
# is based on CONF.scheduler.max_attempts; note that if there are not
|
|
||||||
# enough filtered hosts to provide the full number of alternates, the
|
|
||||||
# list of hosts may be shorter than this amount.
|
|
||||||
num_alts = (CONF.scheduler.max_attempts - 1
|
|
||||||
if return_alternates else 0)
|
|
||||||
|
|
||||||
if instance_uuids is None or alloc_reqs_by_rp_uuid is None:
|
|
||||||
# If there was a problem communicating with the
|
|
||||||
# placement API, alloc_reqs_by_rp_uuid will be None, so we skip
|
|
||||||
# claiming in that case as well. In the case where instance_uuids
|
|
||||||
# is None, that indicates an older conductor, so we need to return
|
|
||||||
# the objects without alternates. They will be converted back to
|
|
||||||
# the older dict format representing HostState objects.
|
|
||||||
# TODO(stephenfin): Remove this when we bump scheduler the RPC API
|
|
||||||
# version to 5.0
|
|
||||||
return self._legacy_find_hosts(context, num_instances, spec_obj,
|
|
||||||
hosts, num_alts,
|
|
||||||
instance_uuids=instance_uuids)
|
|
||||||
|
|
||||||
# A list of the instance UUIDs that were successfully claimed against
|
|
||||||
# in the placement API. If we are not able to successfully claim for
|
|
||||||
# all involved instances, we use this list to remove those allocations
|
|
||||||
# before returning
|
|
||||||
claimed_instance_uuids = []
|
|
||||||
|
|
||||||
# The list of hosts that have been selected (and claimed).
|
|
||||||
claimed_hosts = []
|
|
||||||
|
|
||||||
for num, instance_uuid in enumerate(instance_uuids):
|
|
||||||
# In a multi-create request, the first request spec from the list
|
|
||||||
# is passed to the scheduler and that request spec's instance_uuid
|
|
||||||
# might not be the same as the instance we're processing, so we
|
|
||||||
# update the instance_uuid in that case before passing the request
|
|
||||||
# spec to filters since at least one filter
|
|
||||||
# (ServerGroupAntiAffinityFilter) depends on that information being
|
|
||||||
# accurate.
|
|
||||||
spec_obj.instance_uuid = instance_uuid
|
|
||||||
# Reset the field so it's not persisted accidentally.
|
|
||||||
spec_obj.obj_reset_changes(['instance_uuid'])
|
|
||||||
|
|
||||||
hosts = self._get_sorted_hosts(spec_obj, hosts, num)
|
|
||||||
if not hosts:
|
|
||||||
# NOTE(jaypipes): If we get here, that means not all instances
|
|
||||||
# in instance_uuids were able to be matched to a selected host.
|
|
||||||
# Any allocations will be cleaned up in the
|
|
||||||
# _ensure_sufficient_hosts() call.
|
|
||||||
break
|
|
||||||
|
|
||||||
# Attempt to claim the resources against one or more resource
|
|
||||||
# providers, looping over the sorted list of possible hosts
|
|
||||||
# looking for an allocation_request that contains that host's
|
|
||||||
# resource provider UUID
|
|
||||||
claimed_host = None
|
|
||||||
for host in hosts:
|
|
||||||
cn_uuid = host.uuid
|
|
||||||
if cn_uuid not in alloc_reqs_by_rp_uuid:
|
|
||||||
msg = ("A host state with uuid = '%s' that did not have a "
|
|
||||||
"matching allocation_request was encountered while "
|
|
||||||
"scheduling. This host was skipped.")
|
|
||||||
LOG.debug(msg, cn_uuid)
|
|
||||||
continue
|
|
||||||
|
|
||||||
alloc_reqs = alloc_reqs_by_rp_uuid[cn_uuid]
|
|
||||||
# TODO(jaypipes): Loop through all allocation_requests instead
|
|
||||||
# of just trying the first one. For now, since we'll likely
|
|
||||||
# want to order the allocation_requests in the future based on
|
|
||||||
# information in the provider summaries, we'll just try to
|
|
||||||
# claim resources using the first allocation_request
|
|
||||||
alloc_req = alloc_reqs[0]
|
|
||||||
if utils.claim_resources(elevated, self.placement_client,
|
|
||||||
spec_obj, instance_uuid, alloc_req,
|
|
||||||
allocation_request_version=allocation_request_version):
|
|
||||||
claimed_host = host
|
|
||||||
break
|
|
||||||
|
|
||||||
if claimed_host is None:
|
|
||||||
# We weren't able to claim resources in the placement API
|
|
||||||
# for any of the sorted hosts identified. So, clean up any
|
|
||||||
# successfully-claimed resources for prior instances in
|
|
||||||
# this request and return an empty list which will cause
|
|
||||||
# select_destinations() to raise NoValidHost
|
|
||||||
LOG.debug("Unable to successfully claim against any host.")
|
|
||||||
break
|
|
||||||
|
|
||||||
claimed_instance_uuids.append(instance_uuid)
|
|
||||||
claimed_hosts.append(claimed_host)
|
|
||||||
|
|
||||||
# Now consume the resources so the filter/weights will change for
|
|
||||||
# the next instance.
|
|
||||||
self._consume_selected_host(claimed_host, spec_obj,
|
|
||||||
instance_uuid=instance_uuid)
|
|
||||||
|
|
||||||
# Check if we were able to fulfill the request. If not, this call will
|
|
||||||
# raise a NoValidHost exception.
|
|
||||||
self._ensure_sufficient_hosts(context, claimed_hosts, num_instances,
|
|
||||||
claimed_instance_uuids)
|
|
||||||
|
|
||||||
# We have selected and claimed hosts for each instance. Now we need to
|
|
||||||
# find alternates for each host.
|
|
||||||
selections_to_return = self._get_alternate_hosts(
|
|
||||||
claimed_hosts, spec_obj, hosts, num, num_alts,
|
|
||||||
alloc_reqs_by_rp_uuid, allocation_request_version)
|
|
||||||
return selections_to_return
|
|
||||||
|
|
||||||
def _ensure_sufficient_hosts(self, context, hosts, required_count,
|
|
||||||
claimed_uuids=None):
|
|
||||||
"""Checks that we have selected a host for each requested instance. If
|
|
||||||
not, log this failure, remove allocations for any claimed instances,
|
|
||||||
and raise a NoValidHost exception.
|
|
||||||
"""
|
|
||||||
if len(hosts) == required_count:
|
|
||||||
# We have enough hosts.
|
|
||||||
return
|
|
||||||
|
|
||||||
if claimed_uuids:
|
|
||||||
self._cleanup_allocations(context, claimed_uuids)
|
|
||||||
# NOTE(Rui Chen): If multiple creates failed, set the updated time
|
|
||||||
# of selected HostState to None so that these HostStates are
|
|
||||||
# refreshed according to database in next schedule, and release
|
|
||||||
# the resource consumed by instance in the process of selecting
|
|
||||||
# host.
|
|
||||||
for host in hosts:
|
|
||||||
host.updated = None
|
|
||||||
|
|
||||||
# Log the details but don't put those into the reason since
|
|
||||||
# we don't want to give away too much information about our
|
|
||||||
# actual environment.
|
|
||||||
LOG.debug('There are %(hosts)d hosts available but '
|
|
||||||
'%(required_count)d instances requested to build.',
|
|
||||||
{'hosts': len(hosts),
|
|
||||||
'required_count': required_count})
|
|
||||||
reason = _('There are not enough hosts available.')
|
|
||||||
raise exception.NoValidHost(reason=reason)
|
|
||||||
|
|
||||||
def _cleanup_allocations(self, context, instance_uuids):
|
|
||||||
"""Removes allocations for the supplied instance UUIDs."""
|
|
||||||
if not instance_uuids:
|
|
||||||
return
|
|
||||||
LOG.debug("Cleaning up allocations for %s", instance_uuids)
|
|
||||||
for uuid in instance_uuids:
|
|
||||||
self.placement_client.delete_allocation_for_instance(context, uuid)
|
|
||||||
|
|
||||||
def _legacy_find_hosts(self, context, num_instances, spec_obj, hosts,
|
|
||||||
num_alts, instance_uuids=None):
|
|
||||||
"""Find hosts without invoking placement.
|
|
||||||
|
|
||||||
We may not be able to claim if the Placement service is not reachable.
|
|
||||||
Additionally, we may be working with older conductors that don't pass
|
|
||||||
in instance_uuids.
|
|
||||||
"""
|
|
||||||
# The list of hosts selected for each instance
|
|
||||||
selected_hosts = []
|
|
||||||
|
|
||||||
for num in range(num_instances):
|
|
||||||
instance_uuid = instance_uuids[num] if instance_uuids else None
|
|
||||||
if instance_uuid:
|
|
||||||
# Update the RequestSpec.instance_uuid before sending it to
|
|
||||||
# the filters in case we're doing a multi-create request, but
|
|
||||||
# don't persist the change.
|
|
||||||
spec_obj.instance_uuid = instance_uuid
|
|
||||||
spec_obj.obj_reset_changes(['instance_uuid'])
|
|
||||||
hosts = self._get_sorted_hosts(spec_obj, hosts, num)
|
|
||||||
if not hosts:
|
|
||||||
# No hosts left, so break here, and the
|
|
||||||
# _ensure_sufficient_hosts() call below will handle this.
|
|
||||||
break
|
|
||||||
selected_host = hosts[0]
|
|
||||||
selected_hosts.append(selected_host)
|
|
||||||
self._consume_selected_host(selected_host, spec_obj,
|
|
||||||
instance_uuid=instance_uuid)
|
|
||||||
|
|
||||||
# Check if we were able to fulfill the request. If not, this call will
|
|
||||||
# raise a NoValidHost exception.
|
|
||||||
self._ensure_sufficient_hosts(context, selected_hosts, num_instances)
|
|
||||||
|
|
||||||
# This the overall list of values to be returned. There will be one
|
|
||||||
# item per instance, and each item will be a list of Selection objects
|
|
||||||
# representing the selected host along with zero or more alternates
|
|
||||||
# from the same cell.
|
|
||||||
selections_to_return = self._get_alternate_hosts(selected_hosts,
|
|
||||||
spec_obj, hosts, num, num_alts)
|
|
||||||
return selections_to_return
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _consume_selected_host(selected_host, spec_obj, instance_uuid=None):
|
|
||||||
LOG.debug("Selected host: %(host)s", {'host': selected_host},
|
|
||||||
instance_uuid=instance_uuid)
|
|
||||||
selected_host.consume_from_request(spec_obj)
|
|
||||||
# If we have a server group, add the selected host to it for the
|
|
||||||
# (anti-)affinity filters to filter out hosts for subsequent instances
|
|
||||||
# in a multi-create request.
|
|
||||||
if spec_obj.instance_group is not None:
|
|
||||||
spec_obj.instance_group.hosts.append(selected_host.host)
|
|
||||||
# hosts has to be not part of the updates when saving
|
|
||||||
spec_obj.instance_group.obj_reset_changes(['hosts'])
|
|
||||||
# The ServerGroupAntiAffinityFilter also relies on
|
|
||||||
# HostState.instances being accurate within a multi-create request.
|
|
||||||
if instance_uuid and instance_uuid not in selected_host.instances:
|
|
||||||
# Set a stub since ServerGroupAntiAffinityFilter only cares
|
|
||||||
# about the keys.
|
|
||||||
selected_host.instances[instance_uuid] = (
|
|
||||||
objects.Instance(uuid=instance_uuid))
|
|
||||||
|
|
||||||
def _get_alternate_hosts(self, selected_hosts, spec_obj, hosts, index,
|
|
||||||
num_alts, alloc_reqs_by_rp_uuid=None,
|
|
||||||
allocation_request_version=None):
|
|
||||||
# We only need to filter/weigh the hosts again if we're dealing with
|
|
||||||
# more than one instance and are going to be picking alternates.
|
|
||||||
if index > 0 and num_alts > 0:
|
|
||||||
# The selected_hosts have all had resources 'claimed' via
|
|
||||||
# _consume_selected_host, so we need to filter/weigh and sort the
|
|
||||||
# hosts again to get an accurate count for alternates.
|
|
||||||
hosts = self._get_sorted_hosts(spec_obj, hosts, index)
|
|
||||||
# This is the overall list of values to be returned. There will be one
|
|
||||||
# item per instance, and each item will be a list of Selection objects
|
|
||||||
# representing the selected host along with alternates from the same
|
|
||||||
# cell.
|
|
||||||
selections_to_return = []
|
|
||||||
for selected_host in selected_hosts:
|
|
||||||
# This is the list of hosts for one particular instance.
|
|
||||||
if alloc_reqs_by_rp_uuid:
|
|
||||||
selected_alloc_req = alloc_reqs_by_rp_uuid.get(
|
|
||||||
selected_host.uuid)[0]
|
|
||||||
else:
|
|
||||||
selected_alloc_req = None
|
|
||||||
selection = objects.Selection.from_host_state(selected_host,
|
|
||||||
allocation_request=selected_alloc_req,
|
|
||||||
allocation_request_version=allocation_request_version)
|
|
||||||
selected_plus_alts = [selection]
|
|
||||||
cell_uuid = selected_host.cell_uuid
|
|
||||||
# This will populate the alternates with many of the same unclaimed
|
|
||||||
# hosts. This is OK, as it should be rare for a build to fail. And
|
|
||||||
# if there are not enough hosts to fully populate the alternates,
|
|
||||||
# it's fine to return fewer than we'd like. Note that we exclude
|
|
||||||
# any claimed host from consideration as an alternate because it
|
|
||||||
# will have had its resources reduced and will have a much lower
|
|
||||||
# chance of being able to fit another instance on it.
|
|
||||||
for host in hosts:
|
|
||||||
if len(selected_plus_alts) >= num_alts + 1:
|
|
||||||
break
|
|
||||||
if host.cell_uuid == cell_uuid and host not in selected_hosts:
|
|
||||||
if alloc_reqs_by_rp_uuid is not None:
|
|
||||||
alt_uuid = host.uuid
|
|
||||||
if alt_uuid not in alloc_reqs_by_rp_uuid:
|
|
||||||
msg = ("A host state with uuid = '%s' that did "
|
|
||||||
"not have a matching allocation_request "
|
|
||||||
"was encountered while scheduling. This "
|
|
||||||
"host was skipped.")
|
|
||||||
LOG.debug(msg, alt_uuid)
|
|
||||||
continue
|
|
||||||
|
|
||||||
# TODO(jaypipes): Loop through all allocation_requests
|
|
||||||
# instead of just trying the first one. For now, since
|
|
||||||
# we'll likely want to order the allocation_requests in
|
|
||||||
# the future based on information in the provider
|
|
||||||
# summaries, we'll just try to claim resources using
|
|
||||||
# the first allocation_request
|
|
||||||
alloc_req = alloc_reqs_by_rp_uuid[alt_uuid][0]
|
|
||||||
alt_selection = (
|
|
||||||
objects.Selection.from_host_state(host, alloc_req,
|
|
||||||
allocation_request_version))
|
|
||||||
else:
|
|
||||||
alt_selection = objects.Selection.from_host_state(host)
|
|
||||||
selected_plus_alts.append(alt_selection)
|
|
||||||
selections_to_return.append(selected_plus_alts)
|
|
||||||
return selections_to_return
|
|
||||||
|
|
||||||
def _get_sorted_hosts(self, spec_obj, host_states, index):
|
|
||||||
"""Returns a list of HostState objects that match the required
|
|
||||||
scheduling constraints for the request spec object and have been sorted
|
|
||||||
according to the weighers.
|
|
||||||
"""
|
|
||||||
filtered_hosts = self.host_manager.get_filtered_hosts(host_states,
|
|
||||||
spec_obj, index)
|
|
||||||
|
|
||||||
LOG.debug("Filtered %(hosts)s", {'hosts': filtered_hosts})
|
|
||||||
|
|
||||||
if not filtered_hosts:
|
|
||||||
return []
|
|
||||||
|
|
||||||
weighed_hosts = self.host_manager.get_weighed_hosts(filtered_hosts,
|
|
||||||
spec_obj)
|
|
||||||
if CONF.filter_scheduler.shuffle_best_same_weighed_hosts:
|
|
||||||
# NOTE(pas-ha) Randomize best hosts, relying on weighed_hosts
|
|
||||||
# being already sorted by weight in descending order.
|
|
||||||
# This decreases possible contention and rescheduling attempts
|
|
||||||
# when there is a large number of hosts having the same best
|
|
||||||
# weight, especially so when host_subset_size is 1 (default)
|
|
||||||
best_hosts = [w for w in weighed_hosts
|
|
||||||
if w.weight == weighed_hosts[0].weight]
|
|
||||||
random.shuffle(best_hosts)
|
|
||||||
weighed_hosts = best_hosts + weighed_hosts[len(best_hosts):]
|
|
||||||
# Log the weighed hosts before stripping off the wrapper class so that
|
|
||||||
# the weight value gets logged.
|
|
||||||
LOG.debug("Weighed %(hosts)s", {'hosts': weighed_hosts})
|
|
||||||
# Strip off the WeighedHost wrapper class...
|
|
||||||
weighed_hosts = [h.obj for h in weighed_hosts]
|
|
||||||
|
|
||||||
# We randomize the first element in the returned list to alleviate
|
|
||||||
# congestion where the same host is consistently selected among
|
|
||||||
# numerous potential hosts for similar request specs.
|
|
||||||
host_subset_size = CONF.filter_scheduler.host_subset_size
|
|
||||||
if host_subset_size < len(weighed_hosts):
|
|
||||||
weighed_subset = weighed_hosts[0:host_subset_size]
|
|
||||||
else:
|
|
||||||
weighed_subset = weighed_hosts
|
|
||||||
chosen_host = random.choice(weighed_subset)
|
|
||||||
weighed_hosts.remove(chosen_host)
|
|
||||||
return [chosen_host] + weighed_hosts
|
|
||||||
|
|
||||||
def _get_all_host_states(self, context, spec_obj, provider_summaries):
|
|
||||||
"""Template method, so a subclass can implement caching."""
|
|
||||||
# The provider_summaries variable will be an empty dict when the
|
|
||||||
# Placement API found no providers that match the requested
|
|
||||||
# constraints, which in turn makes compute_uuids an empty list and
|
|
||||||
# get_host_states_by_uuids will return an empty generator-iterator
|
|
||||||
# also, which will eventually result in a NoValidHost error.
|
|
||||||
# It will be None if we're doing a rebuild since that happens in-place.
|
|
||||||
compute_uuids = None
|
|
||||||
if provider_summaries is not None:
|
|
||||||
compute_uuids = list(provider_summaries.keys())
|
|
||||||
return self.host_manager.get_host_states_by_uuids(context,
|
|
||||||
compute_uuids,
|
|
||||||
spec_obj)
|
|
@ -33,7 +33,7 @@ from nova import objects
|
|||||||
from nova.objects import host_mapping as host_mapping_obj
|
from nova.objects import host_mapping as host_mapping_obj
|
||||||
from nova import quota
|
from nova import quota
|
||||||
from nova.scheduler.client import report
|
from nova.scheduler.client import report
|
||||||
from nova.scheduler import filter_scheduler
|
from nova.scheduler import driver
|
||||||
from nova.scheduler import request_filter
|
from nova.scheduler import request_filter
|
||||||
from nova.scheduler import utils
|
from nova.scheduler import utils
|
||||||
|
|
||||||
@ -56,7 +56,7 @@ class SchedulerManager(manager.Manager):
|
|||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
self.placement_client = report.SchedulerReportClient()
|
self.placement_client = report.SchedulerReportClient()
|
||||||
self.driver = filter_scheduler.FilterScheduler()
|
self.driver = driver.SchedulerDriver()
|
||||||
|
|
||||||
super(SchedulerManager, self).__init__(
|
super(SchedulerManager, self).__init__(
|
||||||
service_name='scheduler', *args, **kwargs
|
service_name='scheduler', *args, **kwargs
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
from nova.scheduler import filter_scheduler
|
from nova.scheduler import driver as scheduler_driver
|
||||||
from nova import test
|
from nova import test
|
||||||
from nova.tests import fixtures as nova_fixtures
|
from nova.tests import fixtures as nova_fixtures
|
||||||
from nova.tests.functional import fixtures as func_fixtures
|
from nova.tests.functional import fixtures as func_fixtures
|
||||||
@ -25,7 +25,7 @@ class AntiAffinityMultiCreateRequest(test.TestCase,
|
|||||||
"max_server_per_host" rule in the group's anti-affinity policy which
|
"max_server_per_host" rule in the group's anti-affinity policy which
|
||||||
allows having more than one server from the same anti-affinity group
|
allows having more than one server from the same anti-affinity group
|
||||||
on the same host. As a result, the scheduler filter logic changed and
|
on the same host. As a result, the scheduler filter logic changed and
|
||||||
a regression was introduced because of how the FilterScheduler is tracking
|
a regression was introduced because of how the scheduler is tracking
|
||||||
which hosts are selected for each instance in a multi-create request.
|
which hosts are selected for each instance in a multi-create request.
|
||||||
|
|
||||||
This test uses a custom weigher to ensure that when creating two servers
|
This test uses a custom weigher to ensure that when creating two servers
|
||||||
@ -71,11 +71,11 @@ class AntiAffinityMultiCreateRequest(test.TestCase,
|
|||||||
group = self.api.post_server_groups(
|
group = self.api.post_server_groups(
|
||||||
{'name': 'test group', 'policy': 'anti-affinity'})
|
{'name': 'test group', 'policy': 'anti-affinity'})
|
||||||
|
|
||||||
# Stub out FilterScheduler._get_alternate_hosts so we can assert what
|
# Stub out Scheduler._get_alternate_hosts so we can assert what
|
||||||
# is coming back for alternate hosts is what we'd expect after the
|
# is coming back for alternate hosts is what we'd expect after the
|
||||||
# initial hosts are selected for each instance.
|
# initial hosts are selected for each instance.
|
||||||
original_get_alternate_hosts = (
|
original_get_alternate_hosts = (
|
||||||
filter_scheduler.FilterScheduler._get_alternate_hosts)
|
scheduler_driver.SchedulerDriver._get_alternate_hosts)
|
||||||
|
|
||||||
def stub_get_alternate_hosts(*a, **kw):
|
def stub_get_alternate_hosts(*a, **kw):
|
||||||
# Intercept the result so we can assert there are no alternates.
|
# Intercept the result so we can assert there are no alternates.
|
||||||
@ -94,8 +94,10 @@ class AntiAffinityMultiCreateRequest(test.TestCase,
|
|||||||
hosts.add(selection_list[0].service_host)
|
hosts.add(selection_list[0].service_host)
|
||||||
self.assertEqual(2, len(hosts), hosts)
|
self.assertEqual(2, len(hosts), hosts)
|
||||||
return selections_to_return
|
return selections_to_return
|
||||||
self.stub_out('nova.scheduler.filter_scheduler.FilterScheduler.'
|
|
||||||
'_get_alternate_hosts', stub_get_alternate_hosts)
|
self.stub_out(
|
||||||
|
'nova.scheduler.driver.SchedulerDriver._get_alternate_hosts',
|
||||||
|
stub_get_alternate_hosts)
|
||||||
|
|
||||||
# Now create two servers in that group.
|
# Now create two servers in that group.
|
||||||
server_req = self._build_server(networks='none')
|
server_req = self._build_server(networks='none')
|
||||||
|
@ -17,10 +17,11 @@ from nova.tests.functional import integrated_helpers
|
|||||||
|
|
||||||
|
|
||||||
class ResizeSameHostDoubledAllocations(
|
class ResizeSameHostDoubledAllocations(
|
||||||
integrated_helpers.ProviderUsageBaseTestCase):
|
integrated_helpers.ProviderUsageBaseTestCase,
|
||||||
|
):
|
||||||
"""Regression test for bug 1790204 introduced in Pike.
|
"""Regression test for bug 1790204 introduced in Pike.
|
||||||
|
|
||||||
Since Pike, the FilterScheduler uses Placement to "claim" resources
|
Since Pike, the scheduler uses Placement to "claim" resources
|
||||||
via allocations during scheduling. During a move operation, the
|
via allocations during scheduling. During a move operation, the
|
||||||
scheduler will claim resources on the selected destination resource
|
scheduler will claim resources on the selected destination resource
|
||||||
provider (compute node). For a same-host resize, this means claiming
|
provider (compute node). For a same-host resize, this means claiming
|
||||||
|
@ -3653,10 +3653,10 @@ class ServerDeleteBuildTests(integrated_helpers.ProviderUsageBaseTestCase):
|
|||||||
networks='none')
|
networks='none')
|
||||||
|
|
||||||
with test.nested(
|
with test.nested(
|
||||||
mock.patch('nova.scheduler.filter_scheduler.FilterScheduler.'
|
mock.patch('nova.scheduler.driver.SchedulerDriver'
|
||||||
'_ensure_sufficient_hosts'),
|
'._ensure_sufficient_hosts'),
|
||||||
mock.patch('nova.conductor.manager.ComputeTaskManager.'
|
mock.patch('nova.conductor.manager.ComputeTaskManager'
|
||||||
'_bury_in_cell0')
|
'._bury_in_cell0'),
|
||||||
) as (mock_suff_hosts, mock_bury):
|
) as (mock_suff_hosts, mock_bury):
|
||||||
mock_suff_hosts.side_effect = test.TestingException('oops')
|
mock_suff_hosts.side_effect = test.TestingException('oops')
|
||||||
server = self.api.post_server({'server': server_req})
|
server = self.api.post_server({'server': server_req})
|
||||||
|
@ -12,9 +12,6 @@
|
|||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
"""
|
|
||||||
Tests For Filter Scheduler.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import mock
|
import mock
|
||||||
from oslo_serialization import jsonutils
|
from oslo_serialization import jsonutils
|
||||||
@ -23,7 +20,7 @@ from oslo_utils.fixture import uuidsentinel as uuids
|
|||||||
from nova import context
|
from nova import context
|
||||||
from nova import exception
|
from nova import exception
|
||||||
from nova import objects
|
from nova import objects
|
||||||
from nova.scheduler import filter_scheduler
|
from nova.scheduler import driver as scheduler_driver
|
||||||
from nova.scheduler import host_manager
|
from nova.scheduler import host_manager
|
||||||
from nova.scheduler import utils as scheduler_utils
|
from nova.scheduler import utils as scheduler_utils
|
||||||
from nova.scheduler import weights
|
from nova.scheduler import weights
|
||||||
@ -51,8 +48,8 @@ fake_selection = objects.Selection(service_host="fake_host",
|
|||||||
allocation_request_version=fake_alloc_version)
|
allocation_request_version=fake_alloc_version)
|
||||||
|
|
||||||
|
|
||||||
class FilterSchedulerTestCase(test.NoDBTestCase):
|
class SchedulerTestCase(test.NoDBTestCase):
|
||||||
"""Test case for Filter Scheduler."""
|
"""Test case for scheduler driver."""
|
||||||
|
|
||||||
@mock.patch.object(host_manager.HostManager, '_init_instance_info',
|
@mock.patch.object(host_manager.HostManager, '_init_instance_info',
|
||||||
new=mock.Mock())
|
new=mock.Mock())
|
||||||
@ -63,20 +60,19 @@ class FilterSchedulerTestCase(test.NoDBTestCase):
|
|||||||
@mock.patch('nova.scheduler.client.query.SchedulerQueryClient',
|
@mock.patch('nova.scheduler.client.query.SchedulerQueryClient',
|
||||||
autospec=True)
|
autospec=True)
|
||||||
def setUp(self, mock_sch_query, mock_sch_report):
|
def setUp(self, mock_sch_query, mock_sch_report):
|
||||||
super(FilterSchedulerTestCase, self).setUp()
|
super().setUp()
|
||||||
|
|
||||||
self.driver = filter_scheduler.FilterScheduler()
|
self.driver = scheduler_driver.SchedulerDriver()
|
||||||
self.context = context.RequestContext('fake_user', 'fake_project')
|
self.context = context.RequestContext('fake_user', 'fake_project')
|
||||||
self.topic = 'fake_topic'
|
self.topic = 'fake_topic'
|
||||||
self.servicegroup_api = servicegroup.API()
|
self.servicegroup_api = servicegroup.API()
|
||||||
|
|
||||||
@mock.patch('nova.scheduler.utils.claim_resources')
|
@mock.patch('nova.scheduler.utils.claim_resources')
|
||||||
@mock.patch('nova.scheduler.filter_scheduler.FilterScheduler.'
|
@mock.patch('nova.scheduler.driver.SchedulerDriver._get_all_host_states')
|
||||||
'_get_all_host_states')
|
@mock.patch('nova.scheduler.driver.SchedulerDriver._get_sorted_hosts')
|
||||||
@mock.patch('nova.scheduler.filter_scheduler.FilterScheduler.'
|
def test_schedule_placement_bad_comms(
|
||||||
'_get_sorted_hosts')
|
self, mock_get_hosts, mock_get_all_states, mock_claim,
|
||||||
def test_schedule_placement_bad_comms(self, mock_get_hosts,
|
):
|
||||||
mock_get_all_states, mock_claim):
|
|
||||||
"""If there was a problem communicating with the Placement service,
|
"""If there was a problem communicating with the Placement service,
|
||||||
alloc_reqs_by_rp_uuid will be None and we need to avoid trying to claim
|
alloc_reqs_by_rp_uuid will be None and we need to avoid trying to claim
|
||||||
in the Placement API.
|
in the Placement API.
|
||||||
@ -138,12 +134,11 @@ class FilterSchedulerTestCase(test.NoDBTestCase):
|
|||||||
spec_obj.obj_what_changed())
|
spec_obj.obj_what_changed())
|
||||||
|
|
||||||
@mock.patch('nova.scheduler.utils.claim_resources')
|
@mock.patch('nova.scheduler.utils.claim_resources')
|
||||||
@mock.patch('nova.scheduler.filter_scheduler.FilterScheduler.'
|
@mock.patch('nova.scheduler.driver.SchedulerDriver._get_all_host_states')
|
||||||
'_get_all_host_states')
|
@mock.patch('nova.scheduler.driver.SchedulerDriver._get_sorted_hosts')
|
||||||
@mock.patch('nova.scheduler.filter_scheduler.FilterScheduler.'
|
def test_schedule_old_conductor(
|
||||||
'_get_sorted_hosts')
|
self, mock_get_hosts, mock_get_all_states, mock_claim,
|
||||||
def test_schedule_old_conductor(self, mock_get_hosts,
|
):
|
||||||
mock_get_all_states, mock_claim):
|
|
||||||
"""Old conductor can call scheduler without the instance_uuids
|
"""Old conductor can call scheduler without the instance_uuids
|
||||||
parameter. When this happens, we need to ensure we do not attempt to
|
parameter. When this happens, we need to ensure we do not attempt to
|
||||||
claim resources in the placement API since obviously we need instance
|
claim resources in the placement API since obviously we need instance
|
||||||
@ -196,12 +191,11 @@ class FilterSchedulerTestCase(test.NoDBTestCase):
|
|||||||
self.assertEqual(0, len(host_state.instances))
|
self.assertEqual(0, len(host_state.instances))
|
||||||
|
|
||||||
@mock.patch('nova.scheduler.utils.claim_resources')
|
@mock.patch('nova.scheduler.utils.claim_resources')
|
||||||
@mock.patch('nova.scheduler.filter_scheduler.FilterScheduler.'
|
@mock.patch('nova.scheduler.driver.SchedulerDriver._get_all_host_states')
|
||||||
'_get_all_host_states')
|
@mock.patch('nova.scheduler.driver.SchedulerDriver._get_sorted_hosts')
|
||||||
@mock.patch('nova.scheduler.filter_scheduler.FilterScheduler.'
|
def _test_schedule_successful_claim(
|
||||||
'_get_sorted_hosts')
|
self, mock_get_hosts, mock_get_all_states, mock_claim, num_instances=1,
|
||||||
def _test_schedule_successful_claim(self, mock_get_hosts,
|
):
|
||||||
mock_get_all_states, mock_claim, num_instances=1):
|
|
||||||
spec_obj = objects.RequestSpec(
|
spec_obj = objects.RequestSpec(
|
||||||
num_instances=num_instances,
|
num_instances=num_instances,
|
||||||
flavor=objects.Flavor(memory_mb=512,
|
flavor=objects.Flavor(memory_mb=512,
|
||||||
@ -263,15 +257,13 @@ class FilterSchedulerTestCase(test.NoDBTestCase):
|
|||||||
"""
|
"""
|
||||||
self._test_schedule_successful_claim(num_instances=3)
|
self._test_schedule_successful_claim(num_instances=3)
|
||||||
|
|
||||||
@mock.patch('nova.scheduler.filter_scheduler.FilterScheduler.'
|
@mock.patch('nova.scheduler.driver.SchedulerDriver._cleanup_allocations')
|
||||||
'_cleanup_allocations')
|
|
||||||
@mock.patch('nova.scheduler.utils.claim_resources')
|
@mock.patch('nova.scheduler.utils.claim_resources')
|
||||||
@mock.patch('nova.scheduler.filter_scheduler.FilterScheduler.'
|
@mock.patch('nova.scheduler.driver.SchedulerDriver._get_all_host_states')
|
||||||
'_get_all_host_states')
|
@mock.patch('nova.scheduler.driver.SchedulerDriver._get_sorted_hosts')
|
||||||
@mock.patch('nova.scheduler.filter_scheduler.FilterScheduler.'
|
def test_schedule_unsuccessful_claim(
|
||||||
'_get_sorted_hosts')
|
self, mock_get_hosts, mock_get_all_states, mock_claim, mock_cleanup,
|
||||||
def test_schedule_unsuccessful_claim(self, mock_get_hosts,
|
):
|
||||||
mock_get_all_states, mock_claim, mock_cleanup):
|
|
||||||
"""Tests that we return an empty list if we are unable to successfully
|
"""Tests that we return an empty list if we are unable to successfully
|
||||||
claim resources for the instance
|
claim resources for the instance
|
||||||
"""
|
"""
|
||||||
@ -319,15 +311,13 @@ class FilterSchedulerTestCase(test.NoDBTestCase):
|
|||||||
# Ensure that we have consumed the resources on the chosen host states
|
# Ensure that we have consumed the resources on the chosen host states
|
||||||
self.assertFalse(host_state.consume_from_request.called)
|
self.assertFalse(host_state.consume_from_request.called)
|
||||||
|
|
||||||
@mock.patch('nova.scheduler.filter_scheduler.FilterScheduler.'
|
@mock.patch('nova.scheduler.driver.SchedulerDriver._cleanup_allocations')
|
||||||
'_cleanup_allocations')
|
|
||||||
@mock.patch('nova.scheduler.utils.claim_resources')
|
@mock.patch('nova.scheduler.utils.claim_resources')
|
||||||
@mock.patch('nova.scheduler.filter_scheduler.FilterScheduler.'
|
@mock.patch('nova.scheduler.driver.SchedulerDriver._get_all_host_states')
|
||||||
'_get_all_host_states')
|
@mock.patch('nova.scheduler.driver.SchedulerDriver._get_sorted_hosts')
|
||||||
@mock.patch('nova.scheduler.filter_scheduler.FilterScheduler.'
|
def test_schedule_not_all_instance_clean_claimed(
|
||||||
'_get_sorted_hosts')
|
self, mock_get_hosts, mock_get_all_states, mock_claim, mock_cleanup,
|
||||||
def test_schedule_not_all_instance_clean_claimed(self, mock_get_hosts,
|
):
|
||||||
mock_get_all_states, mock_claim, mock_cleanup):
|
|
||||||
"""Tests that we clean up previously-allocated instances if not all
|
"""Tests that we clean up previously-allocated instances if not all
|
||||||
instances could be scheduled
|
instances could be scheduled
|
||||||
"""
|
"""
|
||||||
@ -373,12 +363,11 @@ class FilterSchedulerTestCase(test.NoDBTestCase):
|
|||||||
mock_cleanup.assert_called_once_with(ctx, [uuids.instance1])
|
mock_cleanup.assert_called_once_with(ctx, [uuids.instance1])
|
||||||
|
|
||||||
@mock.patch('nova.scheduler.utils.claim_resources')
|
@mock.patch('nova.scheduler.utils.claim_resources')
|
||||||
@mock.patch('nova.scheduler.filter_scheduler.FilterScheduler.'
|
@mock.patch('nova.scheduler.driver.SchedulerDriver._get_all_host_states')
|
||||||
'_get_all_host_states')
|
@mock.patch('nova.scheduler.driver.SchedulerDriver._get_sorted_hosts')
|
||||||
@mock.patch('nova.scheduler.filter_scheduler.FilterScheduler.'
|
def test_selection_alloc_requests_for_alts(
|
||||||
'_get_sorted_hosts')
|
self, mock_get_hosts, mock_get_all_states, mock_claim,
|
||||||
def test_selection_alloc_requests_for_alts(self, mock_get_hosts,
|
):
|
||||||
mock_get_all_states, mock_claim):
|
|
||||||
spec_obj = objects.RequestSpec(
|
spec_obj = objects.RequestSpec(
|
||||||
num_instances=1,
|
num_instances=1,
|
||||||
flavor=objects.Flavor(memory_mb=512,
|
flavor=objects.Flavor(memory_mb=512,
|
||||||
@ -439,12 +428,11 @@ class FilterSchedulerTestCase(test.NoDBTestCase):
|
|||||||
self.assertEqual(expected_selection, selected_hosts)
|
self.assertEqual(expected_selection, selected_hosts)
|
||||||
|
|
||||||
@mock.patch('nova.scheduler.utils.claim_resources')
|
@mock.patch('nova.scheduler.utils.claim_resources')
|
||||||
@mock.patch('nova.scheduler.filter_scheduler.FilterScheduler.'
|
@mock.patch('nova.scheduler.driver.SchedulerDriver._get_all_host_states')
|
||||||
'_get_all_host_states')
|
@mock.patch('nova.scheduler.driver.SchedulerDriver._get_sorted_hosts')
|
||||||
@mock.patch('nova.scheduler.filter_scheduler.FilterScheduler.'
|
def test_selection_alloc_requests_no_alts(
|
||||||
'_get_sorted_hosts')
|
self, mock_get_hosts, mock_get_all_states, mock_claim,
|
||||||
def test_selection_alloc_requests_no_alts(self, mock_get_hosts,
|
):
|
||||||
mock_get_all_states, mock_claim):
|
|
||||||
spec_obj = objects.RequestSpec(
|
spec_obj = objects.RequestSpec(
|
||||||
num_instances=1,
|
num_instances=1,
|
||||||
flavor=objects.Flavor(memory_mb=512,
|
flavor=objects.Flavor(memory_mb=512,
|
||||||
@ -501,12 +489,11 @@ class FilterSchedulerTestCase(test.NoDBTestCase):
|
|||||||
self.assertEqual(expected_selection, selected_hosts)
|
self.assertEqual(expected_selection, selected_hosts)
|
||||||
|
|
||||||
@mock.patch('nova.scheduler.utils.claim_resources')
|
@mock.patch('nova.scheduler.utils.claim_resources')
|
||||||
@mock.patch('nova.scheduler.filter_scheduler.FilterScheduler.'
|
@mock.patch('nova.scheduler.driver.SchedulerDriver._get_all_host_states')
|
||||||
'_get_all_host_states')
|
@mock.patch('nova.scheduler.driver.SchedulerDriver._get_sorted_hosts')
|
||||||
@mock.patch('nova.scheduler.filter_scheduler.FilterScheduler.'
|
def test_schedule_instance_group(
|
||||||
'_get_sorted_hosts')
|
self, mock_get_hosts, mock_get_all_states, mock_claim,
|
||||||
def test_schedule_instance_group(self, mock_get_hosts,
|
):
|
||||||
mock_get_all_states, mock_claim):
|
|
||||||
"""Test that since the request spec object contains an instance group
|
"""Test that since the request spec object contains an instance group
|
||||||
object, that upon choosing a host in the primary schedule loop,
|
object, that upon choosing a host in the primary schedule loop,
|
||||||
that we update the request spec's instance group information
|
that we update the request spec's instance group information
|
||||||
@ -599,7 +586,7 @@ class FilterSchedulerTestCase(test.NoDBTestCase):
|
|||||||
self.assertEqual(0, len(spec_obj.obj_what_changed()),
|
self.assertEqual(0, len(spec_obj.obj_what_changed()),
|
||||||
spec_obj.obj_what_changed())
|
spec_obj.obj_what_changed())
|
||||||
|
|
||||||
@mock.patch('nova.scheduler.filter_scheduler.LOG.debug')
|
@mock.patch('nova.scheduler.driver.LOG.debug')
|
||||||
@mock.patch('random.choice', side_effect=lambda x: x[1])
|
@mock.patch('random.choice', side_effect=lambda x: x[1])
|
||||||
@mock.patch('nova.scheduler.host_manager.HostManager.get_weighed_hosts')
|
@mock.patch('nova.scheduler.host_manager.HostManager.get_weighed_hosts')
|
||||||
@mock.patch('nova.scheduler.host_manager.HostManager.get_filtered_hosts')
|
@mock.patch('nova.scheduler.host_manager.HostManager.get_filtered_hosts')
|
||||||
@ -786,8 +773,7 @@ class FilterSchedulerTestCase(test.NoDBTestCase):
|
|||||||
self.assertEqual(['host', 'node'],
|
self.assertEqual(['host', 'node'],
|
||||||
filter_properties['retry']['hosts'][0])
|
filter_properties['retry']['hosts'][0])
|
||||||
|
|
||||||
@mock.patch('nova.scheduler.filter_scheduler.FilterScheduler.'
|
@mock.patch('nova.scheduler.driver.SchedulerDriver._schedule')
|
||||||
'_schedule')
|
|
||||||
def test_select_destinations_match_num_instances(self, mock_schedule):
|
def test_select_destinations_match_num_instances(self, mock_schedule):
|
||||||
"""Tests that the select_destinations() method returns the list of
|
"""Tests that the select_destinations() method returns the list of
|
||||||
hosts from the _schedule() method when the number of returned hosts
|
hosts from the _schedule() method when the number of returned hosts
|
||||||
@ -819,8 +805,7 @@ class FilterSchedulerTestCase(test.NoDBTestCase):
|
|||||||
mock.sentinel.p_sums, mock.sentinel.ar_version, False)
|
mock.sentinel.p_sums, mock.sentinel.ar_version, False)
|
||||||
self.assertEqual([[fake_selection]], dests)
|
self.assertEqual([[fake_selection]], dests)
|
||||||
|
|
||||||
@mock.patch('nova.scheduler.filter_scheduler.FilterScheduler.'
|
@mock.patch('nova.scheduler.driver.SchedulerDriver._schedule')
|
||||||
'_schedule')
|
|
||||||
def test_select_destinations_for_move_ops(self, mock_schedule):
|
def test_select_destinations_for_move_ops(self, mock_schedule):
|
||||||
"""Tests that the select_destinations() method verifies the number of
|
"""Tests that the select_destinations() method verifies the number of
|
||||||
hosts returned from the _schedule() method against the number of
|
hosts returned from the _schedule() method against the number of
|
||||||
@ -855,12 +840,11 @@ class FilterSchedulerTestCase(test.NoDBTestCase):
|
|||||||
self.assertEqual([[fake_selection]], dests)
|
self.assertEqual([[fake_selection]], dests)
|
||||||
|
|
||||||
@mock.patch('nova.scheduler.utils.claim_resources', return_value=True)
|
@mock.patch('nova.scheduler.utils.claim_resources', return_value=True)
|
||||||
@mock.patch('nova.scheduler.filter_scheduler.FilterScheduler.'
|
@mock.patch('nova.scheduler.driver.SchedulerDriver._get_all_host_states')
|
||||||
'_get_all_host_states')
|
@mock.patch('nova.scheduler.driver.SchedulerDriver._get_sorted_hosts')
|
||||||
@mock.patch('nova.scheduler.filter_scheduler.FilterScheduler.'
|
def test_schedule_fewer_num_instances(
|
||||||
'_get_sorted_hosts')
|
self, mock_get_hosts, mock_get_all_states, mock_claim,
|
||||||
def test_schedule_fewer_num_instances(self, mock_get_hosts,
|
):
|
||||||
mock_get_all_states, mock_claim):
|
|
||||||
"""Tests that the _schedule() method properly handles
|
"""Tests that the _schedule() method properly handles
|
||||||
resetting host state objects and raising NoValidHost when there are not
|
resetting host state objects and raising NoValidHost when there are not
|
||||||
enough hosts available.
|
enough hosts available.
|
||||||
@ -896,12 +880,12 @@ class FilterSchedulerTestCase(test.NoDBTestCase):
|
|||||||
|
|
||||||
@mock.patch("nova.scheduler.host_manager.HostState.consume_from_request")
|
@mock.patch("nova.scheduler.host_manager.HostState.consume_from_request")
|
||||||
@mock.patch('nova.scheduler.utils.claim_resources')
|
@mock.patch('nova.scheduler.utils.claim_resources')
|
||||||
@mock.patch("nova.scheduler.filter_scheduler.FilterScheduler."
|
@mock.patch('nova.scheduler.driver.SchedulerDriver._get_sorted_hosts')
|
||||||
"_get_sorted_hosts")
|
@mock.patch('nova.scheduler.driver.SchedulerDriver._get_all_host_states')
|
||||||
@mock.patch("nova.scheduler.filter_scheduler.FilterScheduler."
|
def _test_alternates_returned(
|
||||||
"_get_all_host_states")
|
self, mock_get_all_hosts, mock_sorted, mock_claim, mock_consume,
|
||||||
def _test_alternates_returned(self, mock_get_all_hosts, mock_sorted,
|
num_instances=2, num_alternates=2,
|
||||||
mock_claim, mock_consume, num_instances=2, num_alternates=2):
|
):
|
||||||
all_host_states = []
|
all_host_states = []
|
||||||
alloc_reqs = {}
|
alloc_reqs = {}
|
||||||
for num in range(10):
|
for num in range(10):
|
||||||
@ -959,12 +943,11 @@ class FilterSchedulerTestCase(test.NoDBTestCase):
|
|||||||
|
|
||||||
@mock.patch("nova.scheduler.host_manager.HostState.consume_from_request")
|
@mock.patch("nova.scheduler.host_manager.HostState.consume_from_request")
|
||||||
@mock.patch('nova.scheduler.utils.claim_resources')
|
@mock.patch('nova.scheduler.utils.claim_resources')
|
||||||
@mock.patch("nova.scheduler.filter_scheduler.FilterScheduler."
|
@mock.patch('nova.scheduler.driver.SchedulerDriver._get_sorted_hosts')
|
||||||
"_get_sorted_hosts")
|
@mock.patch('nova.scheduler.driver.SchedulerDriver._get_all_host_states')
|
||||||
@mock.patch("nova.scheduler.filter_scheduler.FilterScheduler."
|
def test_alternates_same_cell(
|
||||||
"_get_all_host_states")
|
self, mock_get_all_hosts, mock_sorted, mock_claim, mock_consume,
|
||||||
def test_alternates_same_cell(self, mock_get_all_hosts, mock_sorted,
|
):
|
||||||
mock_claim, mock_consume):
|
|
||||||
"""Tests getting alternates plus claims where the hosts are spread
|
"""Tests getting alternates plus claims where the hosts are spread
|
||||||
across two cells.
|
across two cells.
|
||||||
"""
|
"""
|
||||||
@ -1016,12 +999,12 @@ class FilterSchedulerTestCase(test.NoDBTestCase):
|
|||||||
|
|
||||||
@mock.patch("nova.scheduler.host_manager.HostState.consume_from_request")
|
@mock.patch("nova.scheduler.host_manager.HostState.consume_from_request")
|
||||||
@mock.patch('nova.scheduler.utils.claim_resources')
|
@mock.patch('nova.scheduler.utils.claim_resources')
|
||||||
@mock.patch("nova.scheduler.filter_scheduler.FilterScheduler."
|
@mock.patch('nova.scheduler.driver.SchedulerDriver._get_sorted_hosts')
|
||||||
"_get_sorted_hosts")
|
@mock.patch('nova.scheduler.driver.SchedulerDriver._get_all_host_states')
|
||||||
@mock.patch("nova.scheduler.filter_scheduler.FilterScheduler."
|
def _test_not_enough_alternates(
|
||||||
"_get_all_host_states")
|
self, mock_get_all_hosts, mock_sorted, mock_claim, mock_consume,
|
||||||
def _test_not_enough_alternates(self, mock_get_all_hosts, mock_sorted,
|
num_hosts, max_attempts,
|
||||||
mock_claim, mock_consume, num_hosts, max_attempts):
|
):
|
||||||
all_host_states = []
|
all_host_states = []
|
||||||
alloc_reqs = {}
|
alloc_reqs = {}
|
||||||
for num in range(num_hosts):
|
for num in range(num_hosts):
|
||||||
@ -1072,9 +1055,10 @@ class FilterSchedulerTestCase(test.NoDBTestCase):
|
|||||||
self._test_not_enough_alternates(num_hosts=20, max_attempts=5)
|
self._test_not_enough_alternates(num_hosts=20, max_attempts=5)
|
||||||
|
|
||||||
@mock.patch('nova.compute.utils.notify_about_scheduler_action')
|
@mock.patch('nova.compute.utils.notify_about_scheduler_action')
|
||||||
@mock.patch.object(filter_scheduler.FilterScheduler, '_schedule')
|
@mock.patch.object(scheduler_driver.SchedulerDriver, '_schedule')
|
||||||
def test_select_destinations_notifications(self, mock_schedule,
|
def test_select_destinations_notifications(
|
||||||
mock_notify):
|
self, mock_schedule, mock_notify,
|
||||||
|
):
|
||||||
mock_schedule.return_value = ([[mock.Mock()]], [[mock.Mock()]])
|
mock_schedule.return_value = ([[mock.Mock()]], [[mock.Mock()]])
|
||||||
|
|
||||||
with mock.patch.object(self.driver.notifier, 'info') as mock_info:
|
with mock.patch.object(self.driver.notifier, 'info') as mock_info:
|
@ -24,7 +24,6 @@ from oslo_utils.fixture import uuidsentinel as uuids
|
|||||||
from nova import context
|
from nova import context
|
||||||
from nova import exception
|
from nova import exception
|
||||||
from nova import objects
|
from nova import objects
|
||||||
from nova.scheduler import filter_scheduler
|
|
||||||
from nova.scheduler import host_manager
|
from nova.scheduler import host_manager
|
||||||
from nova.scheduler import manager
|
from nova.scheduler import manager
|
||||||
from nova import test
|
from nova import test
|
||||||
@ -32,19 +31,6 @@ from nova.tests.unit import fake_server_actions
|
|||||||
from nova.tests.unit.scheduler import fakes
|
from nova.tests.unit.scheduler import fakes
|
||||||
|
|
||||||
|
|
||||||
class SchedulerManagerInitTestCase(test.NoDBTestCase):
|
|
||||||
"""Test case for scheduler manager initiation."""
|
|
||||||
manager_cls = manager.SchedulerManager
|
|
||||||
|
|
||||||
@mock.patch.object(host_manager.HostManager, '_init_instance_info')
|
|
||||||
@mock.patch.object(host_manager.HostManager, '_init_aggregates')
|
|
||||||
def test_init_using_default_schedulerdriver(self,
|
|
||||||
mock_init_agg,
|
|
||||||
mock_init_inst):
|
|
||||||
driver = self.manager_cls().driver
|
|
||||||
self.assertIsInstance(driver, filter_scheduler.FilterScheduler)
|
|
||||||
|
|
||||||
|
|
||||||
class SchedulerManagerTestCase(test.NoDBTestCase):
|
class SchedulerManagerTestCase(test.NoDBTestCase):
|
||||||
"""Test case for scheduler manager."""
|
"""Test case for scheduler manager."""
|
||||||
|
|
||||||
|
@ -24,7 +24,7 @@ CONF = conf.CONF
|
|||||||
|
|
||||||
|
|
||||||
class CrossCellWeigherTestCase(test.NoDBTestCase):
|
class CrossCellWeigherTestCase(test.NoDBTestCase):
|
||||||
"""Tests for the FilterScheduler CrossCellWeigher."""
|
"""Tests for the CrossCellWeigher."""
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super(CrossCellWeigherTestCase, self).setUp()
|
super(CrossCellWeigherTestCase, self).setUp()
|
||||||
|
Loading…
x
Reference in New Issue
Block a user