diff options
author | Stephen Finucane <stephenfin@redhat.com> | 2020-09-25 15:41:22 +0100 |
---|---|---|
committer | Stephen Finucane <stephenfin@redhat.com> | 2021-06-29 12:24:41 +0100 |
commit | e0534cc2893dbe7bf844f7223cfbba84a2a4e782 (patch) | |
tree | 954bb2daa001853f5d0099c99c6bb039d5df731e | |
parent | 6d745036bc98cac674d2740dc20c2029dc201522 (diff) | |
download | nova-e0534cc2893dbe7bf844f7223cfbba84a2a4e782.tar.gz |
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>
-rw-r--r-- | doc/source/contributor/testing/eventlet-profiling.rst | 75 | ||||
-rw-r--r-- | nova/conductor/tasks/migrate.py | 10 | ||||
-rw-r--r-- | nova/scheduler/driver.py | 481 | ||||
-rw-r--r-- | nova/scheduler/filter_scheduler.py | 489 | ||||
-rw-r--r-- | nova/scheduler/manager.py | 4 | ||||
-rw-r--r-- | nova/tests/functional/regressions/test_bug_1781710.py | 14 | ||||
-rw-r--r-- | nova/tests/functional/regressions/test_bug_1790204.py | 5 | ||||
-rw-r--r-- | nova/tests/functional/test_servers.py | 8 | ||||
-rw-r--r-- | nova/tests/unit/scheduler/test_driver.py (renamed from nova/tests/unit/scheduler/test_filter_scheduler.py) | 168 | ||||
-rw-r--r-- | nova/tests/unit/scheduler/test_manager.py | 14 | ||||
-rw-r--r-- | nova/tests/unit/scheduler/weights/test_cross_cell.py | 2 |
11 files changed, 589 insertions, 681 deletions
diff --git a/doc/source/contributor/testing/eventlet-profiling.rst b/doc/source/contributor/testing/eventlet-profiling.rst index 7c1e0b8a8d..a7ebece82a 100644 --- a/doc/source/contributor/testing/eventlet-profiling.rst +++ b/doc/source/contributor/testing/eventlet-profiling.rst @@ -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, ``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 -instance uuids. +instance UUIDs. The process for profiling is: @@ -100,52 +100,19 @@ profiling and benchmarking scenarios so not all changes are relevant here): [notifications] 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 - - diff --git a/nova/scheduler/filter_scheduler.py b/nova/scheduler/filter_scheduler.py - index 672f23077e..cb0f87fe48 100644 - --- a/nova/scheduler/filter_scheduler.py - +++ b/nova/scheduler/filter_scheduler.py - @@ -49,92 +49,99 @@ class FilterScheduler(driver.Scheduler): - 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`. + diff --git nova/scheduler/driver.py nova/scheduler/driver.py + index 555236e8a1..efa84b5a47 100644 + --- nova/scheduler/driver.py + +++ nova/scheduler/driver.py + @@ -95,6 +95,10 @@ class SchedulerDriver: + determined by the configuration option + `CONF.scheduler.max_attempts`. """ + from eventlet.green import profile + pr = profile.Profile() @@ -153,27 +120,19 @@ Change the code in ``nova/scheduler/filter_scheduler.py`` as follows: + 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( + {'request_spec': spec_obj.to_legacy_request_spec_dict()}) + @@ -114,6 +118,10 @@ class SchedulerDriver: context=context, request_spec=spec_obj, action=fields_obj.NotificationAction.SELECT_DESTINATIONS, phase=fields_obj.NotificationPhase.END) + + + pr.stop() + pr.dump_stats('/tmp/select_destinations/%s.prof' % ':'.join(instance_uuids)) + return host_selections + def _schedule( + 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. @@ -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 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 `interactive mode`_:: diff --git a/nova/conductor/tasks/migrate.py b/nova/conductor/tasks/migrate.py index af1ef5af6a..38e10ff4cc 100644 --- a/nova/conductor/tasks/migrate.py +++ b/nova/conductor/tasks/migrate.py @@ -60,10 +60,12 @@ def replace_allocation_with_migration(context, instance, migration): context, instance.uuid)['allocations'] root_alloc = orig_alloc.get(source_cn.uuid, {}).get('resources', {}) if not root_alloc: - LOG.debug('Unable to find existing allocations for instance on ' - 'source compute node: %s. This is normal if you are not ' - 'using the FilterScheduler.', source_cn.uuid, - instance=instance) + # TODO(stephenfin): This was a valid code path when there was support + # for multiple schedulers, but it should probably be an error now + LOG.debug( + 'Unable to find existing allocations for instance on ' + 'source compute node: %s', + source_cn.uuid, instance=instance) return None, None # FIXME(gibi): This method is flawed in that it does not handle allocations diff --git a/nova/scheduler/driver.py b/nova/scheduler/driver.py index 83e0765606..cc1df85bf1 100644 --- a/nova/scheduler/driver.py +++ b/nova/scheduler/driver.py @@ -16,27 +16,490 @@ # 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 utils from nova import servicegroup +CONF = nova.conf.CONF +LOG = logging.getLogger(__name__) + + +class SchedulerDriver: + """The scheduler driver. -class Scheduler(metaclass=abc.ABCMeta): - """The base class that all Scheduler classes should inherit from.""" + Filters and weighs compute hosts to determine the best host to schedule an + instance to. + """ def __init__(self): self.host_manager = host_manager.HostManager() self.servicegroup_api = servicegroup.API() + self.notifier = rpc.get_notifier('scheduler') + self.placement_client = report.SchedulerReportClient() - @abc.abstractmethod - def select_destinations(self, context, spec_obj, instance_uuids, - alloc_reqs_by_rp_uuid, provider_summaries, - allocation_request_version=None, return_alternates=False): + 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 that have been chosen 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`. + """ + 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. """ - return [] + 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) diff --git a/nova/scheduler/filter_scheduler.py b/nova/scheduler/filter_scheduler.py deleted file mode 100644 index c104d82089..0000000000 --- a/nova/scheduler/filter_scheduler.py +++ /dev/null @@ -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) diff --git a/nova/scheduler/manager.py b/nova/scheduler/manager.py index 2a9b923cee..fa2a96e4ce 100644 --- a/nova/scheduler/manager.py +++ b/nova/scheduler/manager.py @@ -33,7 +33,7 @@ from nova import objects from nova.objects import host_mapping as host_mapping_obj from nova import quota 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 utils @@ -56,7 +56,7 @@ class SchedulerManager(manager.Manager): def __init__(self, *args, **kwargs): self.placement_client = report.SchedulerReportClient() - self.driver = filter_scheduler.FilterScheduler() + self.driver = driver.SchedulerDriver() super(SchedulerManager, self).__init__( service_name='scheduler', *args, **kwargs diff --git a/nova/tests/functional/regressions/test_bug_1781710.py b/nova/tests/functional/regressions/test_bug_1781710.py index cb6dda7332..59f95c9d99 100644 --- a/nova/tests/functional/regressions/test_bug_1781710.py +++ b/nova/tests/functional/regressions/test_bug_1781710.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -from nova.scheduler import filter_scheduler +from nova.scheduler import driver as scheduler_driver from nova import test from nova.tests import fixtures as nova_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 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 - 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. 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( {'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 # initial hosts are selected for each instance. original_get_alternate_hosts = ( - filter_scheduler.FilterScheduler._get_alternate_hosts) + scheduler_driver.SchedulerDriver._get_alternate_hosts) def stub_get_alternate_hosts(*a, **kw): # 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) self.assertEqual(2, len(hosts), hosts) 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. server_req = self._build_server(networks='none') diff --git a/nova/tests/functional/regressions/test_bug_1790204.py b/nova/tests/functional/regressions/test_bug_1790204.py index 2b59b7e5a7..f52aed54ca 100644 --- a/nova/tests/functional/regressions/test_bug_1790204.py +++ b/nova/tests/functional/regressions/test_bug_1790204.py @@ -17,10 +17,11 @@ from nova.tests.functional import integrated_helpers class ResizeSameHostDoubledAllocations( - integrated_helpers.ProviderUsageBaseTestCase): + integrated_helpers.ProviderUsageBaseTestCase, +): """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 scheduler will claim resources on the selected destination resource provider (compute node). For a same-host resize, this means claiming diff --git a/nova/tests/functional/test_servers.py b/nova/tests/functional/test_servers.py index 2cba010af7..58ea700ab2 100644 --- a/nova/tests/functional/test_servers.py +++ b/nova/tests/functional/test_servers.py @@ -3653,10 +3653,10 @@ class ServerDeleteBuildTests(integrated_helpers.ProviderUsageBaseTestCase): networks='none') with test.nested( - mock.patch('nova.scheduler.filter_scheduler.FilterScheduler.' - '_ensure_sufficient_hosts'), - mock.patch('nova.conductor.manager.ComputeTaskManager.' - '_bury_in_cell0') + mock.patch('nova.scheduler.driver.SchedulerDriver' + '._ensure_sufficient_hosts'), + mock.patch('nova.conductor.manager.ComputeTaskManager' + '._bury_in_cell0'), ) as (mock_suff_hosts, mock_bury): mock_suff_hosts.side_effect = test.TestingException('oops') server = self.api.post_server({'server': server_req}) diff --git a/nova/tests/unit/scheduler/test_filter_scheduler.py b/nova/tests/unit/scheduler/test_driver.py index c2f39c9c0f..e6dbfcc1db 100644 --- a/nova/tests/unit/scheduler/test_filter_scheduler.py +++ b/nova/tests/unit/scheduler/test_driver.py @@ -12,9 +12,6 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. -""" -Tests For Filter Scheduler. -""" import mock from oslo_serialization import jsonutils @@ -23,7 +20,7 @@ from oslo_utils.fixture import uuidsentinel as uuids from nova import context from nova import exception 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 utils as scheduler_utils from nova.scheduler import weights @@ -51,8 +48,8 @@ fake_selection = objects.Selection(service_host="fake_host", allocation_request_version=fake_alloc_version) -class FilterSchedulerTestCase(test.NoDBTestCase): - """Test case for Filter Scheduler.""" +class SchedulerTestCase(test.NoDBTestCase): + """Test case for scheduler driver.""" @mock.patch.object(host_manager.HostManager, '_init_instance_info', new=mock.Mock()) @@ -63,20 +60,19 @@ class FilterSchedulerTestCase(test.NoDBTestCase): @mock.patch('nova.scheduler.client.query.SchedulerQueryClient', autospec=True) 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.topic = 'fake_topic' self.servicegroup_api = servicegroup.API() @mock.patch('nova.scheduler.utils.claim_resources') - @mock.patch('nova.scheduler.filter_scheduler.FilterScheduler.' - '_get_all_host_states') - @mock.patch('nova.scheduler.filter_scheduler.FilterScheduler.' - '_get_sorted_hosts') - def test_schedule_placement_bad_comms(self, mock_get_hosts, - mock_get_all_states, mock_claim): + @mock.patch('nova.scheduler.driver.SchedulerDriver._get_all_host_states') + @mock.patch('nova.scheduler.driver.SchedulerDriver._get_sorted_hosts') + 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, alloc_reqs_by_rp_uuid will be None and we need to avoid trying to claim in the Placement API. @@ -138,12 +134,11 @@ class FilterSchedulerTestCase(test.NoDBTestCase): spec_obj.obj_what_changed()) @mock.patch('nova.scheduler.utils.claim_resources') - @mock.patch('nova.scheduler.filter_scheduler.FilterScheduler.' - '_get_all_host_states') - @mock.patch('nova.scheduler.filter_scheduler.FilterScheduler.' - '_get_sorted_hosts') - def test_schedule_old_conductor(self, mock_get_hosts, - mock_get_all_states, mock_claim): + @mock.patch('nova.scheduler.driver.SchedulerDriver._get_all_host_states') + @mock.patch('nova.scheduler.driver.SchedulerDriver._get_sorted_hosts') + def test_schedule_old_conductor( + self, mock_get_hosts, mock_get_all_states, mock_claim, + ): """Old conductor can call scheduler without the instance_uuids parameter. When this happens, we need to ensure we do not attempt to 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)) @mock.patch('nova.scheduler.utils.claim_resources') - @mock.patch('nova.scheduler.filter_scheduler.FilterScheduler.' - '_get_all_host_states') - @mock.patch('nova.scheduler.filter_scheduler.FilterScheduler.' - '_get_sorted_hosts') - def _test_schedule_successful_claim(self, mock_get_hosts, - mock_get_all_states, mock_claim, num_instances=1): + @mock.patch('nova.scheduler.driver.SchedulerDriver._get_all_host_states') + @mock.patch('nova.scheduler.driver.SchedulerDriver._get_sorted_hosts') + def _test_schedule_successful_claim( + self, mock_get_hosts, mock_get_all_states, mock_claim, num_instances=1, + ): spec_obj = objects.RequestSpec( num_instances=num_instances, flavor=objects.Flavor(memory_mb=512, @@ -263,15 +257,13 @@ class FilterSchedulerTestCase(test.NoDBTestCase): """ self._test_schedule_successful_claim(num_instances=3) - @mock.patch('nova.scheduler.filter_scheduler.FilterScheduler.' - '_cleanup_allocations') + @mock.patch('nova.scheduler.driver.SchedulerDriver._cleanup_allocations') @mock.patch('nova.scheduler.utils.claim_resources') - @mock.patch('nova.scheduler.filter_scheduler.FilterScheduler.' - '_get_all_host_states') - @mock.patch('nova.scheduler.filter_scheduler.FilterScheduler.' - '_get_sorted_hosts') - def test_schedule_unsuccessful_claim(self, mock_get_hosts, - mock_get_all_states, mock_claim, mock_cleanup): + @mock.patch('nova.scheduler.driver.SchedulerDriver._get_all_host_states') + @mock.patch('nova.scheduler.driver.SchedulerDriver._get_sorted_hosts') + 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 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 self.assertFalse(host_state.consume_from_request.called) - @mock.patch('nova.scheduler.filter_scheduler.FilterScheduler.' - '_cleanup_allocations') + @mock.patch('nova.scheduler.driver.SchedulerDriver._cleanup_allocations') @mock.patch('nova.scheduler.utils.claim_resources') - @mock.patch('nova.scheduler.filter_scheduler.FilterScheduler.' - '_get_all_host_states') - @mock.patch('nova.scheduler.filter_scheduler.FilterScheduler.' - '_get_sorted_hosts') - def test_schedule_not_all_instance_clean_claimed(self, mock_get_hosts, - mock_get_all_states, mock_claim, mock_cleanup): + @mock.patch('nova.scheduler.driver.SchedulerDriver._get_all_host_states') + @mock.patch('nova.scheduler.driver.SchedulerDriver._get_sorted_hosts') + 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 instances could be scheduled """ @@ -373,12 +363,11 @@ class FilterSchedulerTestCase(test.NoDBTestCase): mock_cleanup.assert_called_once_with(ctx, [uuids.instance1]) @mock.patch('nova.scheduler.utils.claim_resources') - @mock.patch('nova.scheduler.filter_scheduler.FilterScheduler.' - '_get_all_host_states') - @mock.patch('nova.scheduler.filter_scheduler.FilterScheduler.' - '_get_sorted_hosts') - def test_selection_alloc_requests_for_alts(self, mock_get_hosts, - mock_get_all_states, mock_claim): + @mock.patch('nova.scheduler.driver.SchedulerDriver._get_all_host_states') + @mock.patch('nova.scheduler.driver.SchedulerDriver._get_sorted_hosts') + def test_selection_alloc_requests_for_alts( + self, mock_get_hosts, mock_get_all_states, mock_claim, + ): spec_obj = objects.RequestSpec( num_instances=1, flavor=objects.Flavor(memory_mb=512, @@ -439,12 +428,11 @@ class FilterSchedulerTestCase(test.NoDBTestCase): self.assertEqual(expected_selection, selected_hosts) @mock.patch('nova.scheduler.utils.claim_resources') - @mock.patch('nova.scheduler.filter_scheduler.FilterScheduler.' - '_get_all_host_states') - @mock.patch('nova.scheduler.filter_scheduler.FilterScheduler.' - '_get_sorted_hosts') - def test_selection_alloc_requests_no_alts(self, mock_get_hosts, - mock_get_all_states, mock_claim): + @mock.patch('nova.scheduler.driver.SchedulerDriver._get_all_host_states') + @mock.patch('nova.scheduler.driver.SchedulerDriver._get_sorted_hosts') + def test_selection_alloc_requests_no_alts( + self, mock_get_hosts, mock_get_all_states, mock_claim, + ): spec_obj = objects.RequestSpec( num_instances=1, flavor=objects.Flavor(memory_mb=512, @@ -501,12 +489,11 @@ class FilterSchedulerTestCase(test.NoDBTestCase): self.assertEqual(expected_selection, selected_hosts) @mock.patch('nova.scheduler.utils.claim_resources') - @mock.patch('nova.scheduler.filter_scheduler.FilterScheduler.' - '_get_all_host_states') - @mock.patch('nova.scheduler.filter_scheduler.FilterScheduler.' - '_get_sorted_hosts') - def test_schedule_instance_group(self, mock_get_hosts, - mock_get_all_states, mock_claim): + @mock.patch('nova.scheduler.driver.SchedulerDriver._get_all_host_states') + @mock.patch('nova.scheduler.driver.SchedulerDriver._get_sorted_hosts') + 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 object, that upon choosing a host in the primary schedule loop, 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()), 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('nova.scheduler.host_manager.HostManager.get_weighed_hosts') @mock.patch('nova.scheduler.host_manager.HostManager.get_filtered_hosts') @@ -786,8 +773,7 @@ class FilterSchedulerTestCase(test.NoDBTestCase): self.assertEqual(['host', 'node'], filter_properties['retry']['hosts'][0]) - @mock.patch('nova.scheduler.filter_scheduler.FilterScheduler.' - '_schedule') + @mock.patch('nova.scheduler.driver.SchedulerDriver._schedule') def test_select_destinations_match_num_instances(self, mock_schedule): """Tests that the select_destinations() method returns the list of 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) self.assertEqual([[fake_selection]], dests) - @mock.patch('nova.scheduler.filter_scheduler.FilterScheduler.' - '_schedule') + @mock.patch('nova.scheduler.driver.SchedulerDriver._schedule') def test_select_destinations_for_move_ops(self, mock_schedule): """Tests that the select_destinations() method verifies 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) @mock.patch('nova.scheduler.utils.claim_resources', return_value=True) - @mock.patch('nova.scheduler.filter_scheduler.FilterScheduler.' - '_get_all_host_states') - @mock.patch('nova.scheduler.filter_scheduler.FilterScheduler.' - '_get_sorted_hosts') - def test_schedule_fewer_num_instances(self, mock_get_hosts, - mock_get_all_states, mock_claim): + @mock.patch('nova.scheduler.driver.SchedulerDriver._get_all_host_states') + @mock.patch('nova.scheduler.driver.SchedulerDriver._get_sorted_hosts') + def test_schedule_fewer_num_instances( + self, mock_get_hosts, mock_get_all_states, mock_claim, + ): """Tests that the _schedule() method properly handles resetting host state objects and raising NoValidHost when there are not 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.utils.claim_resources') - @mock.patch("nova.scheduler.filter_scheduler.FilterScheduler." - "_get_sorted_hosts") - @mock.patch("nova.scheduler.filter_scheduler.FilterScheduler." - "_get_all_host_states") - def _test_alternates_returned(self, mock_get_all_hosts, mock_sorted, - mock_claim, mock_consume, num_instances=2, num_alternates=2): + @mock.patch('nova.scheduler.driver.SchedulerDriver._get_sorted_hosts') + @mock.patch('nova.scheduler.driver.SchedulerDriver._get_all_host_states') + def _test_alternates_returned( + self, mock_get_all_hosts, mock_sorted, mock_claim, mock_consume, + num_instances=2, num_alternates=2, + ): all_host_states = [] alloc_reqs = {} 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.utils.claim_resources') - @mock.patch("nova.scheduler.filter_scheduler.FilterScheduler." - "_get_sorted_hosts") - @mock.patch("nova.scheduler.filter_scheduler.FilterScheduler." - "_get_all_host_states") - def test_alternates_same_cell(self, mock_get_all_hosts, mock_sorted, - mock_claim, mock_consume): + @mock.patch('nova.scheduler.driver.SchedulerDriver._get_sorted_hosts') + @mock.patch('nova.scheduler.driver.SchedulerDriver._get_all_host_states') + 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 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.utils.claim_resources') - @mock.patch("nova.scheduler.filter_scheduler.FilterScheduler." - "_get_sorted_hosts") - @mock.patch("nova.scheduler.filter_scheduler.FilterScheduler." - "_get_all_host_states") - def _test_not_enough_alternates(self, mock_get_all_hosts, mock_sorted, - mock_claim, mock_consume, num_hosts, max_attempts): + @mock.patch('nova.scheduler.driver.SchedulerDriver._get_sorted_hosts') + @mock.patch('nova.scheduler.driver.SchedulerDriver._get_all_host_states') + def _test_not_enough_alternates( + self, mock_get_all_hosts, mock_sorted, mock_claim, mock_consume, + num_hosts, max_attempts, + ): all_host_states = [] alloc_reqs = {} 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) @mock.patch('nova.compute.utils.notify_about_scheduler_action') - @mock.patch.object(filter_scheduler.FilterScheduler, '_schedule') - def test_select_destinations_notifications(self, mock_schedule, - mock_notify): + @mock.patch.object(scheduler_driver.SchedulerDriver, '_schedule') + def test_select_destinations_notifications( + self, mock_schedule, mock_notify, + ): mock_schedule.return_value = ([[mock.Mock()]], [[mock.Mock()]]) with mock.patch.object(self.driver.notifier, 'info') as mock_info: diff --git a/nova/tests/unit/scheduler/test_manager.py b/nova/tests/unit/scheduler/test_manager.py index fdb1933a80..9b3033597d 100644 --- a/nova/tests/unit/scheduler/test_manager.py +++ b/nova/tests/unit/scheduler/test_manager.py @@ -24,7 +24,6 @@ from oslo_utils.fixture import uuidsentinel as uuids from nova import context from nova import exception from nova import objects -from nova.scheduler import filter_scheduler from nova.scheduler import host_manager from nova.scheduler import manager from nova import test @@ -32,19 +31,6 @@ from nova.tests.unit import fake_server_actions 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): """Test case for scheduler manager.""" diff --git a/nova/tests/unit/scheduler/weights/test_cross_cell.py b/nova/tests/unit/scheduler/weights/test_cross_cell.py index ea732ffd20..25ab20211e 100644 --- a/nova/tests/unit/scheduler/weights/test_cross_cell.py +++ b/nova/tests/unit/scheduler/weights/test_cross_cell.py @@ -24,7 +24,7 @@ CONF = conf.CONF class CrossCellWeigherTestCase(test.NoDBTestCase): - """Tests for the FilterScheduler CrossCellWeigher.""" + """Tests for the CrossCellWeigher.""" def setUp(self): super(CrossCellWeigherTestCase, self).setUp() |