Scheduler calling the Placement API
Instead of iterating over the long list of compute nodes in the DB, the scheduler should rather just call the Placement API for getting the list of ResourceProviders that support the resource amounts given by the RequestSpec. Implements: blueprint resource-providers-scheduler-db-filters Depends-On: I573149b9415da2a8bb3951a4c4ce71c4c3e48c6f Change-Id: Ie12acb76ec5affba536c3c45fbb6de35d64aea1b
This commit is contained in:
parent
d486315e0c
commit
4660333d0d
@ -36,6 +36,7 @@ def main():
|
|||||||
logging.setup(CONF, "nova")
|
logging.setup(CONF, "nova")
|
||||||
utils.monkey_patch()
|
utils.monkey_patch()
|
||||||
objects.register_all()
|
objects.register_all()
|
||||||
|
objects.Service.enable_min_version_cache()
|
||||||
|
|
||||||
gmr.TextGuruMeditation.setup_autorun(version)
|
gmr.TextGuruMeditation.setup_autorun(version)
|
||||||
|
|
||||||
|
@ -402,8 +402,12 @@ allocation_ratio_opts = [
|
|||||||
default=0.0,
|
default=0.0,
|
||||||
min=0.0,
|
min=0.0,
|
||||||
help="""
|
help="""
|
||||||
This option helps you specify virtual CPU to physical CPU allocation
|
This option helps you specify virtual CPU to physical CPU allocation ratio.
|
||||||
ratio which affects all CPU filters.
|
|
||||||
|
From Ocata (15.0.0) this is used to influence the hosts selected by
|
||||||
|
the Placement API. Note that when Placement is used, the CoreFilter
|
||||||
|
is redundant, because the Placement API will have already filtered
|
||||||
|
out hosts that would have failed the CoreFilter.
|
||||||
|
|
||||||
This configuration specifies ratio for CoreFilter which can be set
|
This configuration specifies ratio for CoreFilter which can be set
|
||||||
per compute node. For AggregateCoreFilter, it will fall back to this
|
per compute node. For AggregateCoreFilter, it will fall back to this
|
||||||
@ -423,7 +427,12 @@ Possible values:
|
|||||||
min=0.0,
|
min=0.0,
|
||||||
help="""
|
help="""
|
||||||
This option helps you specify virtual RAM to physical RAM
|
This option helps you specify virtual RAM to physical RAM
|
||||||
allocation ratio which affects all RAM filters.
|
allocation ratio.
|
||||||
|
|
||||||
|
From Ocata (15.0.0) this is used to influence the hosts selected by
|
||||||
|
the Placement API. Note that when Placement is used, the RamFilter
|
||||||
|
is redundant, because the Placement API will have already filtered
|
||||||
|
out hosts that would have failed the RamFilter.
|
||||||
|
|
||||||
This configuration specifies ratio for RamFilter which can be set
|
This configuration specifies ratio for RamFilter which can be set
|
||||||
per compute node. For AggregateRamFilter, it will fall back to this
|
per compute node. For AggregateRamFilter, it will fall back to this
|
||||||
@ -443,8 +452,12 @@ Possible values:
|
|||||||
min=0.0,
|
min=0.0,
|
||||||
help="""
|
help="""
|
||||||
This option helps you specify virtual disk to physical disk
|
This option helps you specify virtual disk to physical disk
|
||||||
allocation ratio used by the disk_filter.py script to determine if
|
allocation ratio.
|
||||||
a host has sufficient disk space to fit a requested instance.
|
|
||||||
|
From Ocata (15.0.0) this is used to influence the hosts selected by
|
||||||
|
the Placement API. Note that when Placement is used, the DiskFilter
|
||||||
|
is redundant, because the Placement API will have already filtered
|
||||||
|
out hosts that would have failed the DiskFilter.
|
||||||
|
|
||||||
A ratio greater than 1.0 will result in over-subscription of the
|
A ratio greater than 1.0 will result in over-subscription of the
|
||||||
available physical disk, which can be useful for more
|
available physical disk, which can be useful for more
|
||||||
|
@ -62,7 +62,7 @@ class CachingScheduler(filter_scheduler.FilterScheduler):
|
|||||||
# fetch the list of hosts.
|
# fetch the list of hosts.
|
||||||
self.all_host_states = self._get_up_hosts(elevated)
|
self.all_host_states = self._get_up_hosts(elevated)
|
||||||
|
|
||||||
def _get_all_host_states(self, context):
|
def _get_all_host_states(self, context, spec_obj):
|
||||||
"""Called from the filter scheduler, in a template pattern."""
|
"""Called from the filter scheduler, in a template pattern."""
|
||||||
if self.all_host_states is None:
|
if self.all_host_states is None:
|
||||||
# NOTE(johngarbutt) We only get here when we a scheduler request
|
# NOTE(johngarbutt) We only get here when we a scheduler request
|
||||||
|
@ -15,6 +15,7 @@
|
|||||||
|
|
||||||
import functools
|
import functools
|
||||||
import re
|
import re
|
||||||
|
from six.moves.urllib import parse
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from keystoneauth1 import exceptions as ks_exc
|
from keystoneauth1 import exceptions as ks_exc
|
||||||
@ -208,6 +209,43 @@ class SchedulerReportClient(object):
|
|||||||
url,
|
url,
|
||||||
endpoint_filter=self.ks_filter, raise_exc=False)
|
endpoint_filter=self.ks_filter, raise_exc=False)
|
||||||
|
|
||||||
|
# TODO(sbauza): Change that poor interface into passing a rich versioned
|
||||||
|
# object that would provide the ResourceProvider requirements.
|
||||||
|
@safe_connect
|
||||||
|
def get_filtered_resource_providers(self, filters):
|
||||||
|
"""Returns a list of ResourceProviders matching the requirements
|
||||||
|
expressed by the filters argument, which can include a dict named
|
||||||
|
'resources' where amounts are keyed by resource class names.
|
||||||
|
|
||||||
|
eg. filters = {'resources': {'VCPU': 1}}
|
||||||
|
"""
|
||||||
|
resources = filters.pop("resources", None)
|
||||||
|
if resources:
|
||||||
|
resource_query = ",".join(sorted("%s:%s" % (rc, amount)
|
||||||
|
for (rc, amount) in resources.items()))
|
||||||
|
filters['resources'] = resource_query
|
||||||
|
resp = self.get("/resource_providers?%s" % parse.urlencode(filters),
|
||||||
|
version='1.4')
|
||||||
|
if resp.status_code == 200:
|
||||||
|
data = resp.json()
|
||||||
|
raw_rps = data.get('resource_providers', [])
|
||||||
|
rps = [objects.ResourceProvider(uuid=rp['uuid'],
|
||||||
|
name=rp['name'],
|
||||||
|
generation=rp['generation'],
|
||||||
|
) for rp in raw_rps]
|
||||||
|
return objects.ResourceProviderList(objects=rps)
|
||||||
|
else:
|
||||||
|
msg = _LE("Failed to retrieve filtered list of resource providers "
|
||||||
|
"from placement API for filters %(filters)s. "
|
||||||
|
"Got %(status_code)d: %(err_text)s.")
|
||||||
|
args = {
|
||||||
|
'filters': filters,
|
||||||
|
'status_code': resp.status_code,
|
||||||
|
'err_text': resp.text,
|
||||||
|
}
|
||||||
|
LOG.error(msg, args)
|
||||||
|
return None
|
||||||
|
|
||||||
@safe_connect
|
@safe_connect
|
||||||
def _get_provider_aggregates(self, rp_uuid):
|
def _get_provider_aggregates(self, rp_uuid):
|
||||||
"""Queries the placement API for a resource provider's aggregates.
|
"""Queries the placement API for a resource provider's aggregates.
|
||||||
|
@ -27,7 +27,10 @@ from six.moves import range
|
|||||||
import nova.conf
|
import nova.conf
|
||||||
from nova import exception
|
from nova import exception
|
||||||
from nova.i18n import _
|
from nova.i18n import _
|
||||||
|
from nova import objects
|
||||||
|
from nova.objects import fields
|
||||||
from nova import rpc
|
from nova import rpc
|
||||||
|
from nova.scheduler import client as scheduler_client
|
||||||
from nova.scheduler import driver
|
from nova.scheduler import driver
|
||||||
|
|
||||||
|
|
||||||
@ -40,6 +43,10 @@ class FilterScheduler(driver.Scheduler):
|
|||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(FilterScheduler, self).__init__(*args, **kwargs)
|
super(FilterScheduler, self).__init__(*args, **kwargs)
|
||||||
self.notifier = rpc.get_notifier('scheduler')
|
self.notifier = rpc.get_notifier('scheduler')
|
||||||
|
# TODO(sbauza): It seems weird that we load a scheduler client for
|
||||||
|
# the FilterScheduler but it will be the PlacementClient later on once
|
||||||
|
# we split the needed methods into a separate library.
|
||||||
|
self.scheduler_client = scheduler_client.SchedulerClient()
|
||||||
|
|
||||||
def select_destinations(self, context, spec_obj):
|
def select_destinations(self, context, spec_obj):
|
||||||
"""Selects a filtered set of hosts and nodes."""
|
"""Selects a filtered set of hosts and nodes."""
|
||||||
@ -93,7 +100,7 @@ class FilterScheduler(driver.Scheduler):
|
|||||||
# Note: remember, we are using an iterator here. So only
|
# Note: remember, we are using an iterator here. So only
|
||||||
# traverse this list once. This can bite you if the hosts
|
# traverse this list once. This can bite you if the hosts
|
||||||
# are being scanned in a filter or weighing function.
|
# are being scanned in a filter or weighing function.
|
||||||
hosts = self._get_all_host_states(elevated)
|
hosts = self._get_all_host_states(elevated, spec_obj)
|
||||||
|
|
||||||
selected_hosts = []
|
selected_hosts = []
|
||||||
num_instances = spec_obj.num_instances
|
num_instances = spec_obj.num_instances
|
||||||
@ -129,6 +136,57 @@ class FilterScheduler(driver.Scheduler):
|
|||||||
spec_obj.instance_group.obj_reset_changes(['hosts'])
|
spec_obj.instance_group.obj_reset_changes(['hosts'])
|
||||||
return selected_hosts
|
return selected_hosts
|
||||||
|
|
||||||
def _get_all_host_states(self, context):
|
def _get_resources_per_request_spec(self, spec_obj):
|
||||||
|
resources = {}
|
||||||
|
|
||||||
|
resources[fields.ResourceClass.VCPU] = spec_obj.vcpus
|
||||||
|
resources[fields.ResourceClass.MEMORY_MB] = spec_obj.memory_mb
|
||||||
|
|
||||||
|
requested_disk_mb = (1024 * (spec_obj.root_gb +
|
||||||
|
spec_obj.ephemeral_gb) +
|
||||||
|
spec_obj.swap)
|
||||||
|
# NOTE(sbauza): Disk request is expressed in MB but we count
|
||||||
|
# resources in GB. Since there could be a remainder of the division
|
||||||
|
# by 1024, we need to ceil the result to the next bigger Gb so we
|
||||||
|
# can be sure there would be enough disk space in the destination
|
||||||
|
# to sustain the request.
|
||||||
|
# FIXME(sbauza): All of that could be using math.ceil() but since
|
||||||
|
# we support both py2 and py3, let's fake it until we only support
|
||||||
|
# py3.
|
||||||
|
requested_disk_gb = requested_disk_mb // 1024
|
||||||
|
if requested_disk_mb % 1024 != 0:
|
||||||
|
# Let's ask for a bit more space since we count in GB
|
||||||
|
requested_disk_gb += 1
|
||||||
|
# NOTE(sbauza): Some flavors provide zero size for disk values, we need
|
||||||
|
# to avoid asking for disk usage.
|
||||||
|
if requested_disk_gb != 0:
|
||||||
|
resources[fields.ResourceClass.DISK_GB] = requested_disk_gb
|
||||||
|
|
||||||
|
return resources
|
||||||
|
|
||||||
|
def _get_all_host_states(self, context, spec_obj):
|
||||||
"""Template method, so a subclass can implement caching."""
|
"""Template method, so a subclass can implement caching."""
|
||||||
return self.host_manager.get_all_host_states(context)
|
# NOTE(sbauza): Since Newton compute nodes require a configuration
|
||||||
|
# change to request the Placement API, and given it goes against
|
||||||
|
# our rolling upgrade process, we define a graceful period for allowing
|
||||||
|
# clouds that are not fully upgraded to Ocata to still be able to
|
||||||
|
# have instances being scheduled on old nodes.
|
||||||
|
service_version = objects.Service.get_minimum_version(
|
||||||
|
context, 'nova-compute')
|
||||||
|
# TODO(sbauza): Remove that version check in Pike so we fully call
|
||||||
|
# the placement API anyway.
|
||||||
|
if service_version < 16:
|
||||||
|
LOG.debug("Skipping call to placement, as upgrade in progress.")
|
||||||
|
return self.host_manager.get_all_host_states(context)
|
||||||
|
filters = {'resources': self._get_resources_per_request_spec(spec_obj)}
|
||||||
|
reportclient = self.scheduler_client.reportclient
|
||||||
|
rps = reportclient.get_filtered_resource_providers(filters)
|
||||||
|
# NOTE(sbauza): In case the Placement service is not running yet or
|
||||||
|
# when returning an exception, we wouldn't get any ResourceProviders.
|
||||||
|
# If so, let's return an empty list so _schedule would raise a
|
||||||
|
# NoValidHosts.
|
||||||
|
if not rps:
|
||||||
|
return []
|
||||||
|
compute_uuids = [rp.uuid for rp in rps]
|
||||||
|
return self.host_manager.get_host_states_by_uuids(context,
|
||||||
|
compute_uuids)
|
||||||
|
@ -576,17 +576,28 @@ class HostManager(object):
|
|||||||
return self.weight_handler.get_weighed_objects(self.weighers,
|
return self.weight_handler.get_weighed_objects(self.weighers,
|
||||||
hosts, spec_obj)
|
hosts, spec_obj)
|
||||||
|
|
||||||
|
def get_host_states_by_uuids(self, context, compute_uuids):
|
||||||
|
compute_nodes = objects.ComputeNodeList.get_all_by_uuids(context,
|
||||||
|
compute_uuids)
|
||||||
|
return self._get_host_states(context, compute_nodes)
|
||||||
|
|
||||||
def get_all_host_states(self, context):
|
def get_all_host_states(self, context):
|
||||||
"""Returns a list of HostStates that represents all the hosts
|
"""Returns a list of HostStates that represents all the hosts
|
||||||
the HostManager knows about. Also, each of the consumable resources
|
the HostManager knows about. Also, each of the consumable resources
|
||||||
in HostState are pre-populated and adjusted based on data in the db.
|
in HostState are pre-populated and adjusted based on data in the db.
|
||||||
"""
|
"""
|
||||||
|
compute_nodes = objects.ComputeNodeList.get_all(context)
|
||||||
|
return self._get_host_states(context, compute_nodes)
|
||||||
|
|
||||||
|
def _get_host_states(self, context, compute_nodes):
|
||||||
|
"""Returns a tuple of HostStates given a list of computes.
|
||||||
|
|
||||||
|
Also updates the HostStates internal mapping for the HostManager.
|
||||||
|
"""
|
||||||
service_refs = {service.host: service
|
service_refs = {service.host: service
|
||||||
for service in objects.ServiceList.get_by_binary(
|
for service in objects.ServiceList.get_by_binary(
|
||||||
context, 'nova-compute', include_disabled=True)}
|
context, 'nova-compute', include_disabled=True)}
|
||||||
# Get resource usage across the available compute nodes:
|
# Get resource usage across the available compute nodes:
|
||||||
compute_nodes = objects.ComputeNodeList.get_all(context)
|
|
||||||
seen_nodes = set()
|
seen_nodes = set()
|
||||||
for compute in compute_nodes:
|
for compute in compute_nodes:
|
||||||
service = service_refs.get(compute.host)
|
service = service_refs.get(compute.host)
|
||||||
@ -621,7 +632,7 @@ class HostManager(object):
|
|||||||
"from scheduler"), {'host': host, 'node': node})
|
"from scheduler"), {'host': host, 'node': node})
|
||||||
del self.host_state_map[state_key]
|
del self.host_state_map[state_key]
|
||||||
|
|
||||||
return six.itervalues(self.host_state_map)
|
return (self.host_state_map[host] for host in seen_nodes)
|
||||||
|
|
||||||
def _get_aggregates_info(self, host):
|
def _get_aggregates_info(self, host):
|
||||||
return [self.aggs_by_id[agg_id] for agg_id in
|
return [self.aggs_by_id[agg_id] for agg_id in
|
||||||
|
@ -21,6 +21,7 @@ from oslo_log import log as logging
|
|||||||
from nova import context as nova_context
|
from nova import context as nova_context
|
||||||
from nova import objects
|
from nova import objects
|
||||||
from nova import test
|
from nova import test
|
||||||
|
from nova.tests import fixtures as nova_fixtures
|
||||||
from nova.tests.functional.test_servers import ServersTestBase
|
from nova.tests.functional.test_servers import ServersTestBase
|
||||||
from nova.tests.unit import fake_network
|
from nova.tests.unit import fake_network
|
||||||
from nova.tests.unit.virt.libvirt import fake_imagebackend
|
from nova.tests.unit.virt.libvirt import fake_imagebackend
|
||||||
@ -75,6 +76,7 @@ class NUMAServersTest(ServersTestBase):
|
|||||||
'nova.virt.libvirt.guest.libvirt',
|
'nova.virt.libvirt.guest.libvirt',
|
||||||
fakelibvirt))
|
fakelibvirt))
|
||||||
self.useFixture(fakelibvirt.FakeLibvirtFixture())
|
self.useFixture(fakelibvirt.FakeLibvirtFixture())
|
||||||
|
self.useFixture(nova_fixtures.PlacementFixture())
|
||||||
|
|
||||||
def _setup_compute_service(self):
|
def _setup_compute_service(self):
|
||||||
pass
|
pass
|
||||||
|
@ -20,6 +20,7 @@ from oslo_log import log as logging
|
|||||||
|
|
||||||
from nova.objects import fields
|
from nova.objects import fields
|
||||||
from nova import test
|
from nova import test
|
||||||
|
from nova.tests import fixtures as nova_fixtures
|
||||||
from nova.tests.functional.test_servers import ServersTestBase
|
from nova.tests.functional.test_servers import ServersTestBase
|
||||||
from nova.tests.unit import fake_network
|
from nova.tests.unit import fake_network
|
||||||
from nova.tests.unit.virt.libvirt import fake_libvirt_utils
|
from nova.tests.unit.virt.libvirt import fake_libvirt_utils
|
||||||
@ -89,6 +90,8 @@ class SRIOVServersTest(ServersTestBase):
|
|||||||
fakelibvirt))
|
fakelibvirt))
|
||||||
self.useFixture(fakelibvirt.FakeLibvirtFixture())
|
self.useFixture(fakelibvirt.FakeLibvirtFixture())
|
||||||
|
|
||||||
|
self.useFixture(nova_fixtures.PlacementFixture())
|
||||||
|
|
||||||
self.compute_started = False
|
self.compute_started = False
|
||||||
|
|
||||||
def _setup_compute_service(self):
|
def _setup_compute_service(self):
|
||||||
|
@ -74,6 +74,7 @@ class ServerGroupTestBase(test.TestCase,
|
|||||||
self.useFixture(policy_fixture.RealPolicyFixture())
|
self.useFixture(policy_fixture.RealPolicyFixture())
|
||||||
self.useFixture(nova_fixtures.NeutronFixture(self))
|
self.useFixture(nova_fixtures.NeutronFixture(self))
|
||||||
|
|
||||||
|
self.useFixture(nova_fixtures.PlacementFixture())
|
||||||
api_fixture = self.useFixture(nova_fixtures.OSAPIFixture(
|
api_fixture = self.useFixture(nova_fixtures.OSAPIFixture(
|
||||||
api_version='v2.1'))
|
api_version='v2.1'))
|
||||||
|
|
||||||
@ -141,6 +142,10 @@ class ServerGroupTestV21(ServerGroupTestBase):
|
|||||||
self.compute = self.start_service('compute')
|
self.compute = self.start_service('compute')
|
||||||
|
|
||||||
# NOTE(gibi): start a second compute host to be able to test affinity
|
# NOTE(gibi): start a second compute host to be able to test affinity
|
||||||
|
# NOTE(sbauza): Make sure the FakeDriver returns a different nodename
|
||||||
|
# for the second compute node.
|
||||||
|
fake.set_nodes(['host2'])
|
||||||
|
self.addCleanup(fake.restore_nodes)
|
||||||
self.compute2 = self.start_service('compute', host='host2')
|
self.compute2 = self.start_service('compute', host='host2')
|
||||||
fake_network.set_stub_network_methods(self)
|
fake_network.set_stub_network_methods(self)
|
||||||
|
|
||||||
@ -361,6 +366,7 @@ class ServerGroupTestV21(ServerGroupTestBase):
|
|||||||
|
|
||||||
def test_migrate_with_anti_affinity(self):
|
def test_migrate_with_anti_affinity(self):
|
||||||
# Start additional host to test migration with anti-affinity
|
# Start additional host to test migration with anti-affinity
|
||||||
|
fake.set_nodes(['host3'])
|
||||||
self.start_service('compute', host='host3')
|
self.start_service('compute', host='host3')
|
||||||
|
|
||||||
created_group = self.api.post_server_groups(self.anti_affinity)
|
created_group = self.api.post_server_groups(self.anti_affinity)
|
||||||
|
@ -13,6 +13,7 @@
|
|||||||
from keystoneauth1 import exceptions as ks_exc
|
from keystoneauth1 import exceptions as ks_exc
|
||||||
import mock
|
import mock
|
||||||
import six
|
import six
|
||||||
|
from six.moves.urllib import parse
|
||||||
|
|
||||||
import nova.conf
|
import nova.conf
|
||||||
from nova import context
|
from nova import context
|
||||||
@ -268,6 +269,49 @@ class TestProviderOperations(SchedulerReportClientTestCase):
|
|||||||
mock.sentinel.name,
|
mock.sentinel.name,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_get_filtered_resource_providers(self):
|
||||||
|
uuid = uuids.compute_node
|
||||||
|
resp_mock = mock.Mock(status_code=200)
|
||||||
|
json_data = {
|
||||||
|
'resource_providers': [
|
||||||
|
{'uuid': uuid,
|
||||||
|
'name': uuid,
|
||||||
|
'generation': 42}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
filters = {'resources': {'VCPU': 1, 'MEMORY_MB': 1024}}
|
||||||
|
resp_mock.json.return_value = json_data
|
||||||
|
self.ks_sess_mock.get.return_value = resp_mock
|
||||||
|
|
||||||
|
result = self.client.get_filtered_resource_providers(filters)
|
||||||
|
|
||||||
|
expected_provider = objects.ResourceProvider(
|
||||||
|
uuid=uuid,
|
||||||
|
name=uuid,
|
||||||
|
generation=42,
|
||||||
|
)
|
||||||
|
expected_url = '/resource_providers?%s' % parse.urlencode(
|
||||||
|
{'resources': 'MEMORY_MB:1024,VCPU:1'})
|
||||||
|
self.ks_sess_mock.get.assert_called_once_with(
|
||||||
|
expected_url, endpoint_filter=mock.ANY, raise_exc=False,
|
||||||
|
headers={'OpenStack-API-Version': 'placement 1.4'})
|
||||||
|
self.assertTrue(obj_base.obj_equal_prims(expected_provider,
|
||||||
|
result[0]))
|
||||||
|
|
||||||
|
def test_get_filtered_resource_providers_not_found(self):
|
||||||
|
# Ensure _get_resource_provider() just returns None when the placement
|
||||||
|
# API doesn't find a resource provider matching a UUID
|
||||||
|
resp_mock = mock.Mock(status_code=404)
|
||||||
|
self.ks_sess_mock.get.return_value = resp_mock
|
||||||
|
|
||||||
|
result = self.client.get_filtered_resource_providers({'foo': 'bar'})
|
||||||
|
|
||||||
|
expected_url = '/resource_providers?foo=bar'
|
||||||
|
self.ks_sess_mock.get.assert_called_once_with(
|
||||||
|
expected_url, endpoint_filter=mock.ANY, raise_exc=False,
|
||||||
|
headers={'OpenStack-API-Version': 'placement 1.4'})
|
||||||
|
self.assertIsNone(result)
|
||||||
|
|
||||||
def test_get_resource_provider_found(self):
|
def test_get_resource_provider_found(self):
|
||||||
# Ensure _get_resource_provider() returns a ResourceProvider object if
|
# Ensure _get_resource_provider() returns a ResourceProvider object if
|
||||||
# it finds a resource provider record from the placement API
|
# it finds a resource provider record from the placement API
|
||||||
|
@ -22,6 +22,7 @@ import six
|
|||||||
from nova import objects
|
from nova import objects
|
||||||
from nova.scheduler import driver
|
from nova.scheduler import driver
|
||||||
from nova.scheduler import host_manager
|
from nova.scheduler import host_manager
|
||||||
|
from nova.tests import uuidsentinel
|
||||||
|
|
||||||
NUMA_TOPOLOGY = objects.NUMATopology(
|
NUMA_TOPOLOGY = objects.NUMATopology(
|
||||||
cells=[
|
cells=[
|
||||||
@ -122,6 +123,25 @@ COMPUTE_NODES = [
|
|||||||
host='fake', hypervisor_hostname='fake-hyp'),
|
host='fake', hypervisor_hostname='fake-hyp'),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
RESOURCE_PROVIDERS = [
|
||||||
|
objects.ResourceProvider(
|
||||||
|
uuid=uuidsentinel.rp1,
|
||||||
|
name='host1',
|
||||||
|
generation=1),
|
||||||
|
objects.ResourceProvider(
|
||||||
|
uuid=uuidsentinel.rp2,
|
||||||
|
name='host2',
|
||||||
|
generation=1),
|
||||||
|
objects.ResourceProvider(
|
||||||
|
uuid=uuidsentinel.rp3,
|
||||||
|
name='host3',
|
||||||
|
generation=1),
|
||||||
|
objects.ResourceProvider(
|
||||||
|
uuid=uuidsentinel.rp4,
|
||||||
|
name='host4',
|
||||||
|
generation=1),
|
||||||
|
]
|
||||||
|
|
||||||
SERVICES = [
|
SERVICES = [
|
||||||
objects.Service(host='host1', disabled=False),
|
objects.Service(host='host1', disabled=False),
|
||||||
objects.Service(host='host2', disabled=True),
|
objects.Service(host='host2', disabled=True),
|
||||||
|
@ -48,7 +48,7 @@ class CachingSchedulerTestCase(test_scheduler.SchedulerTestCase):
|
|||||||
def test_get_all_host_states_returns_cached_value(self, mock_up_hosts):
|
def test_get_all_host_states_returns_cached_value(self, mock_up_hosts):
|
||||||
self.driver.all_host_states = []
|
self.driver.all_host_states = []
|
||||||
|
|
||||||
self.driver._get_all_host_states(self.context)
|
self.driver._get_all_host_states(self.context, None)
|
||||||
|
|
||||||
self.assertFalse(mock_up_hosts.called)
|
self.assertFalse(mock_up_hosts.called)
|
||||||
self.assertEqual([], self.driver.all_host_states)
|
self.assertEqual([], self.driver.all_host_states)
|
||||||
@ -58,7 +58,7 @@ class CachingSchedulerTestCase(test_scheduler.SchedulerTestCase):
|
|||||||
def test_get_all_host_states_loads_hosts(self, mock_up_hosts):
|
def test_get_all_host_states_loads_hosts(self, mock_up_hosts):
|
||||||
mock_up_hosts.return_value = ["asdf"]
|
mock_up_hosts.return_value = ["asdf"]
|
||||||
|
|
||||||
result = self.driver._get_all_host_states(self.context)
|
result = self.driver._get_all_host_states(self.context, None)
|
||||||
|
|
||||||
self.assertTrue(mock_up_hosts.called)
|
self.assertTrue(mock_up_hosts.called)
|
||||||
self.assertEqual(["asdf"], self.driver.all_host_states)
|
self.assertEqual(["asdf"], self.driver.all_host_states)
|
||||||
|
@ -42,12 +42,18 @@ class FilterSchedulerTestCase(test_scheduler.SchedulerTestCase):
|
|||||||
@mock.patch('nova.objects.ServiceList.get_by_binary',
|
@mock.patch('nova.objects.ServiceList.get_by_binary',
|
||||||
return_value=fakes.SERVICES)
|
return_value=fakes.SERVICES)
|
||||||
@mock.patch('nova.objects.InstanceList.get_by_host')
|
@mock.patch('nova.objects.InstanceList.get_by_host')
|
||||||
@mock.patch('nova.objects.ComputeNodeList.get_all',
|
@mock.patch('nova.objects.ComputeNodeList.get_all_by_uuids',
|
||||||
return_value=fakes.COMPUTE_NODES)
|
return_value=fakes.COMPUTE_NODES)
|
||||||
|
@mock.patch('nova.scheduler.client.report.SchedulerReportClient.'
|
||||||
|
'get_filtered_resource_providers',
|
||||||
|
return_value=fakes.RESOURCE_PROVIDERS)
|
||||||
|
@mock.patch('nova.objects.Service.get_minimum_version',
|
||||||
|
return_value=objects.service.SERVICE_VERSION)
|
||||||
@mock.patch('nova.db.instance_extra_get_by_instance_uuid',
|
@mock.patch('nova.db.instance_extra_get_by_instance_uuid',
|
||||||
return_value={'numa_topology': None,
|
return_value={'numa_topology': None,
|
||||||
'pci_requests': None})
|
'pci_requests': None})
|
||||||
def test_schedule_happy_day(self, mock_get_extra, mock_get_all,
|
def test_schedule_happy_day(self, mock_get_extra, mock_get_mv,
|
||||||
|
mock_get_rps, mock_get_all,
|
||||||
mock_by_host, mock_get_by_binary):
|
mock_by_host, mock_get_by_binary):
|
||||||
"""Make sure there's nothing glaringly wrong with _schedule()
|
"""Make sure there's nothing glaringly wrong with _schedule()
|
||||||
by doing a happy day pass through.
|
by doing a happy day pass through.
|
||||||
@ -68,6 +74,7 @@ class FilterSchedulerTestCase(test_scheduler.SchedulerTestCase):
|
|||||||
flavor=objects.Flavor(memory_mb=512,
|
flavor=objects.Flavor(memory_mb=512,
|
||||||
root_gb=512,
|
root_gb=512,
|
||||||
ephemeral_gb=0,
|
ephemeral_gb=0,
|
||||||
|
swap=0,
|
||||||
vcpus=1),
|
vcpus=1),
|
||||||
project_id=1,
|
project_id=1,
|
||||||
os_type='Linux',
|
os_type='Linux',
|
||||||
@ -115,12 +122,18 @@ class FilterSchedulerTestCase(test_scheduler.SchedulerTestCase):
|
|||||||
@mock.patch('nova.objects.ServiceList.get_by_binary',
|
@mock.patch('nova.objects.ServiceList.get_by_binary',
|
||||||
return_value=fakes.SERVICES)
|
return_value=fakes.SERVICES)
|
||||||
@mock.patch('nova.objects.InstanceList.get_by_host')
|
@mock.patch('nova.objects.InstanceList.get_by_host')
|
||||||
@mock.patch('nova.objects.ComputeNodeList.get_all',
|
@mock.patch('nova.objects.ComputeNodeList.get_all_by_uuids',
|
||||||
return_value=fakes.COMPUTE_NODES)
|
return_value=fakes.COMPUTE_NODES)
|
||||||
|
@mock.patch('nova.scheduler.client.report.SchedulerReportClient.'
|
||||||
|
'get_filtered_resource_providers',
|
||||||
|
return_value=fakes.RESOURCE_PROVIDERS)
|
||||||
|
@mock.patch('nova.objects.Service.get_minimum_version',
|
||||||
|
return_value=objects.service.SERVICE_VERSION)
|
||||||
@mock.patch('nova.db.instance_extra_get_by_instance_uuid',
|
@mock.patch('nova.db.instance_extra_get_by_instance_uuid',
|
||||||
return_value={'numa_topology': None,
|
return_value={'numa_topology': None,
|
||||||
'pci_requests': None})
|
'pci_requests': None})
|
||||||
def test_schedule_host_pool(self, mock_get_extra, mock_get_all,
|
def test_schedule_host_pool(self, mock_get_extra, mock_get_mv,
|
||||||
|
mock_get_rps, mock_get_all,
|
||||||
mock_by_host, mock_get_by_binary):
|
mock_by_host, mock_get_by_binary):
|
||||||
"""Make sure the host_subset_size property works properly."""
|
"""Make sure the host_subset_size property works properly."""
|
||||||
|
|
||||||
@ -134,6 +147,7 @@ class FilterSchedulerTestCase(test_scheduler.SchedulerTestCase):
|
|||||||
flavor=objects.Flavor(root_gb=512,
|
flavor=objects.Flavor(root_gb=512,
|
||||||
memory_mb=512,
|
memory_mb=512,
|
||||||
ephemeral_gb=0,
|
ephemeral_gb=0,
|
||||||
|
swap=0,
|
||||||
vcpus=1),
|
vcpus=1),
|
||||||
pci_requests=None,
|
pci_requests=None,
|
||||||
numa_topology=None,
|
numa_topology=None,
|
||||||
@ -150,12 +164,18 @@ class FilterSchedulerTestCase(test_scheduler.SchedulerTestCase):
|
|||||||
@mock.patch('nova.objects.ServiceList.get_by_binary',
|
@mock.patch('nova.objects.ServiceList.get_by_binary',
|
||||||
return_value=fakes.SERVICES)
|
return_value=fakes.SERVICES)
|
||||||
@mock.patch('nova.objects.InstanceList.get_by_host')
|
@mock.patch('nova.objects.InstanceList.get_by_host')
|
||||||
@mock.patch('nova.objects.ComputeNodeList.get_all',
|
@mock.patch('nova.objects.ComputeNodeList.get_all_by_uuids',
|
||||||
return_value=fakes.COMPUTE_NODES)
|
return_value=fakes.COMPUTE_NODES)
|
||||||
|
@mock.patch('nova.scheduler.client.report.SchedulerReportClient.'
|
||||||
|
'get_filtered_resource_providers',
|
||||||
|
return_value=fakes.RESOURCE_PROVIDERS)
|
||||||
|
@mock.patch('nova.objects.Service.get_minimum_version',
|
||||||
|
return_value=objects.service.SERVICE_VERSION)
|
||||||
@mock.patch('nova.db.instance_extra_get_by_instance_uuid',
|
@mock.patch('nova.db.instance_extra_get_by_instance_uuid',
|
||||||
return_value={'numa_topology': None,
|
return_value={'numa_topology': None,
|
||||||
'pci_requests': None})
|
'pci_requests': None})
|
||||||
def test_schedule_large_host_pool(self, mock_get_extra, mock_get_all,
|
def test_schedule_large_host_pool(self, mock_get_extra, mock_get_mv,
|
||||||
|
mock_get_rps, mock_get_all,
|
||||||
mock_by_host, mock_get_by_binary):
|
mock_by_host, mock_get_by_binary):
|
||||||
"""Hosts should still be chosen if pool size
|
"""Hosts should still be chosen if pool size
|
||||||
is larger than number of filtered hosts.
|
is larger than number of filtered hosts.
|
||||||
@ -171,6 +191,7 @@ class FilterSchedulerTestCase(test_scheduler.SchedulerTestCase):
|
|||||||
flavor=objects.Flavor(root_gb=512,
|
flavor=objects.Flavor(root_gb=512,
|
||||||
memory_mb=512,
|
memory_mb=512,
|
||||||
ephemeral_gb=0,
|
ephemeral_gb=0,
|
||||||
|
swap=0,
|
||||||
vcpus=1),
|
vcpus=1),
|
||||||
pci_requests=None,
|
pci_requests=None,
|
||||||
numa_topology=None,
|
numa_topology=None,
|
||||||
@ -187,12 +208,18 @@ class FilterSchedulerTestCase(test_scheduler.SchedulerTestCase):
|
|||||||
@mock.patch('nova.scheduler.host_manager.HostManager._get_instance_info')
|
@mock.patch('nova.scheduler.host_manager.HostManager._get_instance_info')
|
||||||
@mock.patch('nova.objects.ServiceList.get_by_binary',
|
@mock.patch('nova.objects.ServiceList.get_by_binary',
|
||||||
return_value=fakes.SERVICES)
|
return_value=fakes.SERVICES)
|
||||||
@mock.patch('nova.objects.ComputeNodeList.get_all',
|
@mock.patch('nova.objects.ComputeNodeList.get_all_by_uuids',
|
||||||
return_value=fakes.COMPUTE_NODES)
|
return_value=fakes.COMPUTE_NODES)
|
||||||
|
@mock.patch('nova.scheduler.client.report.SchedulerReportClient.'
|
||||||
|
'get_filtered_resource_providers',
|
||||||
|
return_value=fakes.RESOURCE_PROVIDERS)
|
||||||
|
@mock.patch('nova.objects.Service.get_minimum_version',
|
||||||
|
return_value=objects.service.SERVICE_VERSION)
|
||||||
@mock.patch('nova.db.instance_extra_get_by_instance_uuid',
|
@mock.patch('nova.db.instance_extra_get_by_instance_uuid',
|
||||||
return_value={'numa_topology': None,
|
return_value={'numa_topology': None,
|
||||||
'pci_requests': None})
|
'pci_requests': None})
|
||||||
def test_schedule_chooses_best_host(self, mock_get_extra, mock_cn_get_all,
|
def test_schedule_chooses_best_host(self, mock_get_extra, mock_get_mv,
|
||||||
|
mock_get_rps, mock_cn_get_all,
|
||||||
mock_get_by_binary,
|
mock_get_by_binary,
|
||||||
mock_get_inst_info):
|
mock_get_inst_info):
|
||||||
"""If host_subset_size is 1, the largest host with greatest weight
|
"""If host_subset_size is 1, the largest host with greatest weight
|
||||||
@ -219,6 +246,7 @@ class FilterSchedulerTestCase(test_scheduler.SchedulerTestCase):
|
|||||||
flavor=objects.Flavor(root_gb=512,
|
flavor=objects.Flavor(root_gb=512,
|
||||||
memory_mb=512,
|
memory_mb=512,
|
||||||
ephemeral_gb=0,
|
ephemeral_gb=0,
|
||||||
|
swap=0,
|
||||||
vcpus=1),
|
vcpus=1),
|
||||||
pci_requests=None,
|
pci_requests=None,
|
||||||
numa_topology=None,
|
numa_topology=None,
|
||||||
@ -234,15 +262,33 @@ class FilterSchedulerTestCase(test_scheduler.SchedulerTestCase):
|
|||||||
|
|
||||||
self.assertEqual(50, hosts[0].weight)
|
self.assertEqual(50, hosts[0].weight)
|
||||||
|
|
||||||
|
# TODO(sbauza): Remove that unit test in Pike
|
||||||
|
@mock.patch('nova.objects.Service.get_minimum_version',
|
||||||
|
return_value=15)
|
||||||
|
def test_get_all_host_states_with_newton_computes(self, mock_get_mv):
|
||||||
|
"""If at least one compute node is older than Ocata, then we should
|
||||||
|
not call the placement API.
|
||||||
|
"""
|
||||||
|
with mock.patch.object(self.driver.host_manager,
|
||||||
|
'get_all_host_states') as mock_get_hosts:
|
||||||
|
self.driver._get_all_host_states(self.context, mock.sentinel.spec)
|
||||||
|
mock_get_hosts.assert_called_once_with(self.context)
|
||||||
|
|
||||||
@mock.patch('nova.objects.ServiceList.get_by_binary',
|
@mock.patch('nova.objects.ServiceList.get_by_binary',
|
||||||
return_value=fakes.SERVICES)
|
return_value=fakes.SERVICES)
|
||||||
@mock.patch('nova.objects.InstanceList.get_by_host')
|
@mock.patch('nova.objects.InstanceList.get_by_host')
|
||||||
@mock.patch('nova.objects.ComputeNodeList.get_all',
|
@mock.patch('nova.objects.ComputeNodeList.get_all_by_uuids',
|
||||||
return_value=fakes.COMPUTE_NODES)
|
return_value=fakes.COMPUTE_NODES)
|
||||||
|
@mock.patch('nova.scheduler.client.report.SchedulerReportClient.'
|
||||||
|
'get_filtered_resource_providers',
|
||||||
|
return_value=fakes.RESOURCE_PROVIDERS)
|
||||||
|
@mock.patch('nova.objects.Service.get_minimum_version',
|
||||||
|
return_value=objects.service.SERVICE_VERSION)
|
||||||
@mock.patch('nova.db.instance_extra_get_by_instance_uuid',
|
@mock.patch('nova.db.instance_extra_get_by_instance_uuid',
|
||||||
return_value={'numa_topology': None,
|
return_value={'numa_topology': None,
|
||||||
'pci_requests': None})
|
'pci_requests': None})
|
||||||
def test_select_destinations(self, mock_get_extra, mock_get_all,
|
def test_select_destinations(self, mock_get_extra, mock_get_mv,
|
||||||
|
mock_get_rps, mock_get_all,
|
||||||
mock_by_host, mock_get_by_binary):
|
mock_by_host, mock_get_by_binary):
|
||||||
"""select_destinations is basically a wrapper around _schedule().
|
"""select_destinations is basically a wrapper around _schedule().
|
||||||
|
|
||||||
@ -269,6 +315,7 @@ class FilterSchedulerTestCase(test_scheduler.SchedulerTestCase):
|
|||||||
flavor=objects.Flavor(memory_mb=512,
|
flavor=objects.Flavor(memory_mb=512,
|
||||||
root_gb=512,
|
root_gb=512,
|
||||||
ephemeral_gb=0,
|
ephemeral_gb=0,
|
||||||
|
swap=0,
|
||||||
vcpus=1),
|
vcpus=1),
|
||||||
project_id=1,
|
project_id=1,
|
||||||
os_type='Linux',
|
os_type='Linux',
|
||||||
@ -286,6 +333,7 @@ class FilterSchedulerTestCase(test_scheduler.SchedulerTestCase):
|
|||||||
(host, node) = (dests[0]['host'], dests[0]['nodename'])
|
(host, node) = (dests[0]['host'], dests[0]['nodename'])
|
||||||
self.assertEqual(host, selected_hosts[0])
|
self.assertEqual(host, selected_hosts[0])
|
||||||
self.assertEqual(node, selected_nodes[0])
|
self.assertEqual(node, selected_nodes[0])
|
||||||
|
mock_get_mv.assert_called_once_with(mock.ANY, 'nova-compute')
|
||||||
|
|
||||||
@mock.patch.object(filter_scheduler.FilterScheduler, '_schedule')
|
@mock.patch.object(filter_scheduler.FilterScheduler, '_schedule')
|
||||||
def test_select_destinations_notifications(self, mock_schedule):
|
def test_select_destinations_notifications(self, mock_schedule):
|
||||||
@ -332,3 +380,29 @@ class FilterSchedulerTestCase(test_scheduler.SchedulerTestCase):
|
|||||||
# Make sure that the consumed hosts have chance to be reverted.
|
# Make sure that the consumed hosts have chance to be reverted.
|
||||||
for host in consumed_hosts:
|
for host in consumed_hosts:
|
||||||
self.assertIsNone(host.obj.updated)
|
self.assertIsNone(host.obj.updated)
|
||||||
|
|
||||||
|
def _test_get_resources_per_request_spec(self, flavor, expected):
|
||||||
|
fake_spec = objects.RequestSpec(flavor=flavor)
|
||||||
|
resources = self.driver._get_resources_per_request_spec(fake_spec)
|
||||||
|
self.assertEqual(expected, resources)
|
||||||
|
|
||||||
|
def test_get_resources_per_request_spec(self):
|
||||||
|
flavor = objects.Flavor(vcpus=1,
|
||||||
|
memory_mb=1024,
|
||||||
|
root_gb=10,
|
||||||
|
ephemeral_gb=5,
|
||||||
|
swap=0)
|
||||||
|
expected_resources = {'VCPU': 1,
|
||||||
|
'MEMORY_MB': 1024,
|
||||||
|
'DISK_GB': 15}
|
||||||
|
self._test_get_resources_per_request_spec(flavor, expected_resources)
|
||||||
|
|
||||||
|
def test_get_resources_per_request_spec_with_no_disk(self):
|
||||||
|
flavor = objects.Flavor(vcpus=1,
|
||||||
|
memory_mb=1024,
|
||||||
|
root_gb=0,
|
||||||
|
ephemeral_gb=0,
|
||||||
|
swap=0)
|
||||||
|
expected_resources = {'VCPU': 1,
|
||||||
|
'MEMORY_MB': 1024}
|
||||||
|
self._test_get_resources_per_request_spec(flavor, expected_resources)
|
||||||
|
@ -0,0 +1,18 @@
|
|||||||
|
---
|
||||||
|
features:
|
||||||
|
- nova-scheduler process is now calling the placement API in order to get
|
||||||
|
a list of valid destinations before calling the filters. That works only
|
||||||
|
if all your compute nodes are fully upgraded to Ocata. If some
|
||||||
|
nodes are not upgraded, the scheduler will still lookup from the DB
|
||||||
|
instead which is less performant.
|
||||||
|
upgrade:
|
||||||
|
- Since the Placement service is now mandatory in Ocata, you need to deploy
|
||||||
|
it and amend your compute node configuration with correct placement
|
||||||
|
instructions before restarting nova-compute or the compute node will
|
||||||
|
refuse to start.
|
||||||
|
- If by Newton (14.0.0), you don't use any of the CoreFilter, RamFilter or
|
||||||
|
DiskFilter, then please modify all your compute node's configuration by
|
||||||
|
amending either ``cpu_allocation_ratio`` (if you don't use CoreFilter) or
|
||||||
|
``ram_allocation_ratio`` (if you don't use RamFilter) or
|
||||||
|
``disk_allocation_ratio`` (if you don't use DiskFilter) by putting a 9999.0
|
||||||
|
value for the ratio before upgrading the nova-scheduler to Ocata.
|
Loading…
Reference in New Issue
Block a user