Add a new request filter to isolate aggregates
Added a new request filter 'isolate_aggregates' for filtering of hosts by isolated aggregates. This filter prepares a list of aggregates that should be ignored by the placement service. It checks if aggregates has metadata 'trait:<trait_name>='required' and if these traits <trait_name> are present in flavor extra specs or image properties of the request otherwise all those aggregates will be included in the list of isolated aggregates. The filter is enabled by a new conf option, [scheduler]enable_isolated_aggregate_filtering. Change-Id: I9ab9d7d65378be564b3731b5227ede8cece71bef Implements: blueprint placement-req-filter-forbidden-aggregates
This commit is contained in:
@@ -184,6 +184,17 @@ AvailabilityZoneFilter in the scheduler.
|
||||
This setting causes the scheduler to ask placement only for compute
|
||||
hosts that support the ``disk_format`` of the image used in the request.
|
||||
"""),
|
||||
cfg.BoolOpt("enable_isolated_aggregate_filtering",
|
||||
default=False,
|
||||
help="""
|
||||
This setting allows the scheduler to restrict hosts in aggregates based on
|
||||
matching required traits in the aggregate metadata and the instance
|
||||
flavor/image. If an aggregate is configured with a property with key
|
||||
``trait:$TRAIT_NAME`` and value ``required``, the instance flavor extra_specs
|
||||
and/or image metadata must also contain ``trait:$TRAIT_NAME=required`` to be
|
||||
eligible to be scheduled to hosts in that aggregate. More technical details
|
||||
at https://docs.openstack.org/nova/latest/reference/isolate-aggregates.html
|
||||
""")
|
||||
]
|
||||
|
||||
filter_scheduler_group = cfg.OptGroup(name="filter_scheduler",
|
||||
|
||||
@@ -20,6 +20,7 @@ import nova.conf
|
||||
from nova import exception
|
||||
from nova.i18n import _
|
||||
from nova import objects
|
||||
from nova.scheduler import utils
|
||||
|
||||
|
||||
CONF = nova.conf.CONF
|
||||
@@ -45,6 +46,48 @@ def trace_request_filter(fn):
|
||||
return wrapper
|
||||
|
||||
|
||||
@trace_request_filter
|
||||
def isolate_aggregates(ctxt, request_spec):
|
||||
"""Prepare list of aggregates that should be isolated.
|
||||
|
||||
This filter will prepare the list of aggregates that should be
|
||||
ignored by the placement service. It checks if aggregates has metadata
|
||||
'trait:<trait_name>='required' and if <trait_name> is not present in
|
||||
either of flavor extra specs or image properties, then those aggregates
|
||||
will be included in the list of isolated aggregates.
|
||||
|
||||
Precisely this filter gets the trait request form the image and
|
||||
flavor and unions them. Then it accumulates the set of aggregates that
|
||||
request traits are "non_matching_by_metadata_keys" and uses that to
|
||||
produce the list of isolated aggregates.
|
||||
"""
|
||||
|
||||
if not CONF.scheduler.enable_isolated_aggregate_filtering:
|
||||
return False
|
||||
|
||||
# Get required traits set in flavor and image
|
||||
res_req = utils.ResourceRequest(request_spec)
|
||||
required_traits = res_req.all_required_traits
|
||||
|
||||
keys = ['trait:%s' % trait for trait in required_traits]
|
||||
|
||||
isolated_aggregates = (
|
||||
objects.aggregate.AggregateList.get_non_matching_by_metadata_keys(
|
||||
ctxt, keys, 'trait:', value='required'))
|
||||
|
||||
# Set list of isolated aggregates to destination object of request_spec
|
||||
if isolated_aggregates:
|
||||
if ('requested_destination' not in request_spec or
|
||||
request_spec.requested_destination is None):
|
||||
request_spec.requested_destination = objects.Destination()
|
||||
|
||||
destination = request_spec.requested_destination
|
||||
destination.append_forbidden_aggregates(
|
||||
agg.uuid for agg in isolated_aggregates)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@trace_request_filter
|
||||
def require_tenant_aggregate(ctxt, request_spec):
|
||||
"""Require hosts in an aggregate based on tenant id.
|
||||
@@ -180,6 +223,7 @@ ALL_REQUEST_FILTERS = [
|
||||
map_az_to_placement_aggregate,
|
||||
require_image_type_support,
|
||||
compute_status_filter,
|
||||
isolate_aggregates,
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ from nova.tests.functional import fixtures as func_fixtures
|
||||
from nova.tests.functional import integrated_helpers
|
||||
import nova.tests.unit.image.fake
|
||||
from nova.tests.unit import policy_fixture
|
||||
from nova.tests.unit import utils as test_utils
|
||||
from nova import utils
|
||||
|
||||
CONF = nova.conf.CONF
|
||||
@@ -59,8 +60,8 @@ class AggregatesTest(integrated_helpers._IntegratedTestBase):
|
||||
self.assertEqual(2, self._add_hosts_to_aggregate())
|
||||
|
||||
|
||||
class AggregateRequestFiltersTest(test.TestCase,
|
||||
integrated_helpers.InstanceHelperMixin):
|
||||
class AggregateRequestFiltersTest(
|
||||
integrated_helpers.ProviderUsageBaseTestCase):
|
||||
microversion = 'latest'
|
||||
compute_driver = 'fake.MediumFakeDriver'
|
||||
|
||||
@@ -159,10 +160,12 @@ class AggregateRequestFiltersTest(test.TestCase,
|
||||
|
||||
return server
|
||||
|
||||
def _boot_server(self, az=None):
|
||||
def _boot_server(self, az=None, flavor_id=None, image_id=None):
|
||||
flavor_id = flavor_id or self.flavors[0]['id']
|
||||
image_uuid = image_id or '155d900f-4e14-4e4c-a73d-069cbf4541e6'
|
||||
server_req = self._build_minimal_create_server_request(
|
||||
self.api, 'test-instance', flavor_id=self.flavors[0]['id'],
|
||||
image_uuid='155d900f-4e14-4e4c-a73d-069cbf4541e6',
|
||||
self.api, 'test-instance', flavor_id=flavor_id,
|
||||
image_uuid=image_uuid,
|
||||
networks='none', az=az)
|
||||
|
||||
created_server = self.api.post_server({'server': server_req})
|
||||
@@ -223,6 +226,23 @@ class AggregateRequestFiltersTest(test.TestCase,
|
||||
}
|
||||
self.admin_api.post_aggregate_action(agg['id'], action)
|
||||
|
||||
def _set_traits_on_aggregate(self, agg, traits):
|
||||
"""Set traits to aggregate.
|
||||
|
||||
:param agg: Name of the nova aggregate
|
||||
:param traits: List of traits to be assigned to the aggregate
|
||||
"""
|
||||
action = {
|
||||
'set_metadata': {
|
||||
'metadata': {
|
||||
'trait:' + trait: 'required'
|
||||
for trait in traits
|
||||
}
|
||||
}
|
||||
}
|
||||
self.admin_api.post_aggregate_action(
|
||||
self.aggregates[agg]['id'], action)
|
||||
|
||||
|
||||
class AggregatePostTest(AggregateRequestFiltersTest):
|
||||
|
||||
@@ -420,23 +440,313 @@ class AvailabilityZoneFilterTest(AggregateRequestFiltersTest):
|
||||
self.assertEqual(['host2', 'host2'], hosts)
|
||||
|
||||
|
||||
class IsolateAggregateFilterTest(AggregateRequestFiltersTest):
|
||||
def setUp(self):
|
||||
# Default to enabling the filter
|
||||
self.flags(enable_isolated_aggregate_filtering=True,
|
||||
group='scheduler')
|
||||
|
||||
# Use a custom weigher that would prefer host1 if the isolate
|
||||
# aggregate filter were not in place otherwise it's not deterministic
|
||||
# whether we're landing on host2 because of the filter or just by
|
||||
# chance.
|
||||
self.flags(weight_classes=[__name__ + '.HostNameWeigher'],
|
||||
group='filter_scheduler')
|
||||
|
||||
super(IsolateAggregateFilterTest, self).setUp()
|
||||
self.image_service = nova.tests.unit.image.fake.FakeImageService()
|
||||
# setting traits to flavors
|
||||
flavor_body = {'flavor': {'name': 'test_flavor',
|
||||
'ram': 512,
|
||||
'vcpus': 1,
|
||||
'disk': 1
|
||||
}}
|
||||
self.flavor_with_trait_dxva = self.api.post_flavor(flavor_body)
|
||||
self.admin_api.post_extra_spec(
|
||||
self.flavor_with_trait_dxva['id'],
|
||||
{'extra_specs': {'trait:HW_GPU_API_DXVA': 'required'}})
|
||||
flavor_body['flavor']['name'] = 'test_flavor1'
|
||||
self.flavor_with_trait_sgx = self.api.post_flavor(flavor_body)
|
||||
self.admin_api.post_extra_spec(
|
||||
self.flavor_with_trait_sgx['id'],
|
||||
{'extra_specs': {'trait:HW_CPU_X86_SGX': 'required'}})
|
||||
self.flavor_without_trait = self.flavors[0]
|
||||
|
||||
with nova.utils.temporary_mutation(self.api, microversion='2.35'):
|
||||
images = self.api.get_images()
|
||||
self.image_id_without_trait = images[0]['id']
|
||||
|
||||
def test_filter_with_no_valid_host(self):
|
||||
"""Test 'isolate_aggregates' filter with no valid hosts.
|
||||
|
||||
No required traits set in image/flavor, so all aggregates with
|
||||
required traits set should be ignored.
|
||||
"""
|
||||
|
||||
rp_uuid1 = self._get_provider_uuid_by_host('host1')
|
||||
self._set_provider_traits(
|
||||
rp_uuid1, ['HW_CPU_X86_VMX', 'HW_CPU_X86_SGX'])
|
||||
self._set_traits_on_aggregate(
|
||||
'only-host1', ['HW_CPU_X86_VMX', 'HW_CPU_X86_SGX'])
|
||||
|
||||
rp_uuid2 = self._get_provider_uuid_by_host('host2')
|
||||
self._set_provider_traits(
|
||||
rp_uuid2, ['HW_CPU_X86_VMX', 'HW_CPU_X86_SGX'])
|
||||
self._set_traits_on_aggregate(
|
||||
'only-host2', ['HW_CPU_X86_VMX', 'HW_CPU_X86_SGX'])
|
||||
|
||||
server = self._boot_server(
|
||||
flavor_id=self.flavor_without_trait['id'],
|
||||
image_id=self.image_id_without_trait)
|
||||
self.assertIsNone(self._get_instance_host(server))
|
||||
server = self.api.get_server(server['id'])
|
||||
self.assertEqual('ERROR', server['status'])
|
||||
self.assertIn('No valid host', server['fault']['message'])
|
||||
|
||||
def test_filter_without_trait(self):
|
||||
"""Test 'isolate_aggregates' filter with valid hosts.
|
||||
|
||||
No required traits set in image/flavor so instance should be booted on
|
||||
host from an aggregate with no required traits set.
|
||||
"""
|
||||
|
||||
rp_uuid1 = self._get_provider_uuid_by_host('host1')
|
||||
self._set_provider_traits(
|
||||
rp_uuid1, ['HW_CPU_X86_VMX', 'HW_CPU_X86_SGX'])
|
||||
self._set_traits_on_aggregate(
|
||||
'only-host1', ['HW_CPU_X86_VMX', 'HW_CPU_X86_SGX'])
|
||||
|
||||
server = self._boot_server(
|
||||
flavor_id=self.flavor_without_trait['id'],
|
||||
image_id=self.image_id_without_trait)
|
||||
self.assertEqual('host2', self._get_instance_host(server))
|
||||
|
||||
def test_filter_with_trait_on_flavor(self):
|
||||
"""Test filter with matching required traits set only in one aggregate.
|
||||
|
||||
Required trait (HW_GPU_API_DXVA) set in flavor so instance should be
|
||||
booted on host with matching required traits set on aggregates.
|
||||
"""
|
||||
|
||||
rp_uuid2 = self._get_provider_uuid_by_host('host2')
|
||||
self._set_provider_traits(rp_uuid2, ['HW_GPU_API_DXVA'])
|
||||
|
||||
rp_uuid1 = self._get_provider_uuid_by_host('host1')
|
||||
self._set_provider_traits(
|
||||
rp_uuid1, ['HW_CPU_X86_VMX', 'HW_CPU_X86_SGX'])
|
||||
|
||||
self._set_traits_on_aggregate('only-host2', ['HW_GPU_API_DXVA'])
|
||||
self._set_traits_on_aggregate(
|
||||
'only-host1', ['HW_CPU_X86_VMX', 'HW_CPU_X86_SGX'])
|
||||
server = self._boot_server(
|
||||
flavor_id=self.flavor_with_trait_dxva['id'],
|
||||
image_id=self.image_id_without_trait)
|
||||
|
||||
self.assertEqual('host2', self._get_instance_host(server))
|
||||
|
||||
def test_filter_with_common_trait_on_aggregates(self):
|
||||
"""Test filter with common required traits set to aggregates.
|
||||
|
||||
Required trait (HW_CPU_X86_SGX) set in flavor so instance should be
|
||||
booted on host with exact matching required traits set on aggregates.
|
||||
"""
|
||||
|
||||
rp_uuid2 = self._get_provider_uuid_by_host('host2')
|
||||
self._set_provider_traits(rp_uuid2, ['HW_CPU_X86_SGX'])
|
||||
|
||||
rp_uuid1 = self._get_provider_uuid_by_host('host1')
|
||||
self._set_provider_traits(
|
||||
rp_uuid1, ['HW_CPU_X86_VMX', 'HW_CPU_X86_SGX'])
|
||||
|
||||
self._set_traits_on_aggregate('only-host2', ['HW_CPU_X86_SGX'])
|
||||
self._set_traits_on_aggregate(
|
||||
'only-host1', ['HW_CPU_X86_VMX', 'HW_CPU_X86_SGX'])
|
||||
server = self._boot_server(
|
||||
flavor_id=self.flavor_with_trait_sgx['id'],
|
||||
image_id=self.image_id_without_trait)
|
||||
|
||||
self.assertEqual('host2', self._get_instance_host(server))
|
||||
|
||||
def test_filter_with_traits_on_image_and_flavor(self):
|
||||
"""Test filter with common traits set to image/flavor and aggregates.
|
||||
|
||||
Required trait (HW_CPU_X86_SGX) set in flavor and
|
||||
required trait (HW_CPU_X86_VMX) set in image, so instance should be
|
||||
booted on host with exact matching required traits set on aggregates.
|
||||
"""
|
||||
|
||||
rp_uuid2 = self._get_provider_uuid_by_host('host2')
|
||||
self._set_provider_traits(
|
||||
rp_uuid2, ['HW_CPU_X86_VMX', 'HW_CPU_X86_SGX'])
|
||||
|
||||
rp_uuid1 = self._get_provider_uuid_by_host('host1')
|
||||
self._set_provider_traits(rp_uuid1, ['HW_GPU_API_DXVA'])
|
||||
|
||||
self._set_traits_on_aggregate('only-host1', ['HW_GPU_API_DXVA'])
|
||||
self._set_traits_on_aggregate(
|
||||
'only-host2', ['HW_CPU_X86_VMX', 'HW_CPU_X86_SGX'])
|
||||
|
||||
# Creating a new image and setting traits on it.
|
||||
with nova.utils.temporary_mutation(self.api, microversion='2.35'):
|
||||
self.ctxt = test_utils.get_test_admin_context()
|
||||
img_ref = self.image_service.create(self.ctxt, {'name': 'image10'})
|
||||
image_id_with_trait = img_ref['id']
|
||||
self.addCleanup(
|
||||
self.image_service.delete, self.ctxt, image_id_with_trait)
|
||||
self.api.api_put('/images/%s/metadata' % image_id_with_trait,
|
||||
{'metadata': {
|
||||
'trait:HW_CPU_X86_VMX': 'required'}})
|
||||
server = self._boot_server(
|
||||
flavor_id=self.flavor_with_trait_sgx['id'],
|
||||
image_id=image_id_with_trait)
|
||||
self.assertEqual('host2', self._get_instance_host(server))
|
||||
|
||||
def test_filter_with_traits_image_flavor_subset_of_aggregates(self):
|
||||
"""Test filter with image/flavor required traits subset of aggregates.
|
||||
|
||||
Image and flavor has a nonempty set of required traits that's subset
|
||||
set of the traits on the aggregates.
|
||||
"""
|
||||
|
||||
rp_uuid2 = self._get_provider_uuid_by_host('host2')
|
||||
self._set_provider_traits(
|
||||
rp_uuid2, ['HW_CPU_X86_VMX', 'HW_GPU_API_DXVA', 'HW_CPU_X86_SGX'])
|
||||
|
||||
self._set_traits_on_aggregate(
|
||||
'only-host2',
|
||||
['HW_CPU_X86_VMX', 'HW_GPU_API_DXVA', 'HW_CPU_X86_SGX'])
|
||||
|
||||
# Creating a new image and setting traits on it.
|
||||
with nova.utils.temporary_mutation(self.api, microversion='2.35'):
|
||||
self.ctxt = test_utils.get_test_admin_context()
|
||||
img_ref = self.image_service.create(self.ctxt, {'name': 'image10'})
|
||||
image_id_with_trait = img_ref['id']
|
||||
self.addCleanup(
|
||||
self.image_service.delete, self.ctxt, image_id_with_trait)
|
||||
self.api.api_put('/images/%s/metadata' % image_id_with_trait,
|
||||
{'metadata': {
|
||||
'trait:HW_CPU_X86_VMX': 'required'}})
|
||||
server = self._boot_server(
|
||||
flavor_id=self.flavor_with_trait_sgx['id'],
|
||||
image_id=image_id_with_trait)
|
||||
|
||||
self.assertIsNone(self._get_instance_host(server))
|
||||
server = self.api.get_server(server['id'])
|
||||
self.assertEqual('ERROR', server['status'])
|
||||
self.assertIn('No valid host', server['fault']['message'])
|
||||
|
||||
def test_filter_with_traits_image_flavor_disjoint_of_aggregates(self):
|
||||
"""Test filter with image/flav required traits disjoint of aggregates.
|
||||
|
||||
Image and flavor has a nonempty set of required traits that's disjoint
|
||||
set of the traits on the aggregates.
|
||||
"""
|
||||
|
||||
rp_uuid2 = self._get_provider_uuid_by_host('host2')
|
||||
self._set_provider_traits(rp_uuid2, ['HW_CPU_X86_VMX'])
|
||||
|
||||
rp_uuid1 = self._get_provider_uuid_by_host('host1')
|
||||
self._set_provider_traits(rp_uuid1, ['HW_GPU_API_DXVA'])
|
||||
|
||||
self._set_traits_on_aggregate('only-host1', ['HW_GPU_API_DXVA'])
|
||||
self._set_traits_on_aggregate('only-host2', ['HW_CPU_X86_VMX'])
|
||||
|
||||
# Creating a new image and setting traits on it.
|
||||
with nova.utils.temporary_mutation(self.api, microversion='2.35'):
|
||||
self.ctxt = test_utils.get_test_admin_context()
|
||||
img_ref = self.image_service.create(self.ctxt, {'name': 'image10'})
|
||||
image_id_with_trait = img_ref['id']
|
||||
self.addCleanup(
|
||||
self.image_service.delete, self.ctxt, image_id_with_trait)
|
||||
self.api.api_put('/images/%s/metadata' % image_id_with_trait,
|
||||
{'metadata': {
|
||||
'trait:HW_CPU_X86_VMX': 'required'}})
|
||||
server = self._boot_server(
|
||||
flavor_id=self.flavor_with_trait_sgx['id'],
|
||||
image_id=image_id_with_trait)
|
||||
|
||||
self.assertIsNone(self._get_instance_host(server))
|
||||
server = self.api.get_server(server['id'])
|
||||
self.assertEqual('ERROR', server['status'])
|
||||
self.assertIn('No valid host', server['fault']['message'])
|
||||
|
||||
|
||||
class IsolateAggregateFilterTestWithConcernFilters(IsolateAggregateFilterTest):
|
||||
def setUp(self):
|
||||
filters = CONF.filter_scheduler.enabled_filters
|
||||
|
||||
# NOTE(shilpasd): To test `isolate_aggregates` request filter, along
|
||||
# with following filters which also filters hosts based on aggregate
|
||||
# metadata.
|
||||
if 'AggregateImagePropertiesIsolation' not in filters:
|
||||
filters.append('AggregateImagePropertiesIsolation')
|
||||
if 'AggregateInstanceExtraSpecsFilter' not in filters:
|
||||
filters.append('AggregateInstanceExtraSpecsFilter')
|
||||
self.flags(enabled_filters=filters, group='filter_scheduler')
|
||||
|
||||
super(IsolateAggregateFilterTestWithConcernFilters, self).setUp()
|
||||
|
||||
|
||||
class IsolateAggregateFilterTestWOConcernFilters(IsolateAggregateFilterTest):
|
||||
def setUp(self):
|
||||
filters = CONF.filter_scheduler.enabled_filters
|
||||
|
||||
# NOTE(shilpasd): To test `isolate_aggregates` request filter, removed
|
||||
# following filters which also filters hosts based on aggregate
|
||||
# metadata.
|
||||
if 'AggregateImagePropertiesIsolation' in filters:
|
||||
filters.remove('AggregateImagePropertiesIsolation')
|
||||
if 'AggregateInstanceExtraSpecsFilter' in filters:
|
||||
filters.remove('AggregateInstanceExtraSpecsFilter')
|
||||
self.flags(enabled_filters=filters, group='filter_scheduler')
|
||||
|
||||
super(IsolateAggregateFilterTestWOConcernFilters, self).setUp()
|
||||
|
||||
|
||||
class TestAggregateFiltersTogether(AggregateRequestFiltersTest):
|
||||
def setUp(self):
|
||||
# Use a custom weigher that would prefer host1 if the forbidden
|
||||
# aggregate filter were not in place otherwise it's not deterministic
|
||||
# whether we're landing on host2 because of the filter or just by
|
||||
# chance.
|
||||
self.flags(weight_classes=[__name__ + '.HostNameWeigher'],
|
||||
group='filter_scheduler')
|
||||
|
||||
# NOTE(danms): Do this before calling setUp() so that
|
||||
# the scheduler service that is started sees the new value
|
||||
filters = CONF.filter_scheduler.enabled_filters
|
||||
filters.remove('AvailabilityZoneFilter')
|
||||
|
||||
# NOTE(shilpasd): To test `isolate_aggregates` request filter, removed
|
||||
# following filters which also filters hosts based on aggregate
|
||||
# metadata.
|
||||
if 'AggregateImagePropertiesIsolation' in filters:
|
||||
filters.remove('AggregateImagePropertiesIsolation')
|
||||
if 'AggregateInstanceExtraSpecsFilter' in filters:
|
||||
filters.remove('AggregateInstanceExtraSpecsFilter')
|
||||
self.flags(enabled_filters=filters, group='filter_scheduler')
|
||||
|
||||
super(TestAggregateFiltersTogether, self).setUp()
|
||||
|
||||
# Default to enabling both filters
|
||||
# Default to enabling all filters
|
||||
self.flags(limit_tenants_to_placement_aggregate=True,
|
||||
group='scheduler')
|
||||
self.flags(placement_aggregate_required_for_tenants=True,
|
||||
group='scheduler')
|
||||
self.flags(query_placement_for_availability_zone=True,
|
||||
group='scheduler')
|
||||
self.flags(enable_isolated_aggregate_filtering=True,
|
||||
group='scheduler')
|
||||
# setting traits to flavors
|
||||
flavor_body = {'flavor': {'name': 'test_flavor',
|
||||
'ram': 512,
|
||||
'vcpus': 1,
|
||||
'disk': 1
|
||||
}}
|
||||
self.flavor_with_trait_dxva = self.api.post_flavor(flavor_body)
|
||||
self.admin_api.post_extra_spec(
|
||||
self.flavor_with_trait_dxva['id'],
|
||||
{'extra_specs': {'trait:HW_GPU_API_DXVA': 'required'}})
|
||||
|
||||
def test_tenant_with_az_match(self):
|
||||
# Grant our tenant access to the aggregate with
|
||||
@@ -464,6 +774,41 @@ class TestAggregateFiltersTogether(AggregateRequestFiltersTest):
|
||||
server = self.api.get_server(server['id'])
|
||||
self.assertEqual('ERROR', server['status'])
|
||||
|
||||
def test_tenant_with_az_and_traits_match(self):
|
||||
# Grant our tenant access to the aggregate with host2
|
||||
self._grant_tenant_aggregate('only-host2',
|
||||
[self.api.project_id])
|
||||
# Set an az on only-host2
|
||||
self._set_az_aggregate('only-host2', 'myaz')
|
||||
# Set trait on host2
|
||||
rp_uuid2 = self._get_provider_uuid_by_host('host2')
|
||||
self._set_provider_traits(rp_uuid2, ['HW_GPU_API_DXVA'])
|
||||
# Set trait on aggregate only-host2
|
||||
self._set_traits_on_aggregate('only-host2', ['HW_GPU_API_DXVA'])
|
||||
# Boot the server into that az and make sure we land
|
||||
server = self._boot_server(
|
||||
flavor_id=self.flavor_with_trait_dxva['id'], az='myaz')
|
||||
self.assertEqual('host2', self._get_instance_host(server))
|
||||
|
||||
def test_tenant_with_az_and_traits_mismatch(self):
|
||||
# Grant our tenant access to the aggregate with host2
|
||||
self._grant_tenant_aggregate('only-host2',
|
||||
[self.api.project_id])
|
||||
# Set an az on only-host1
|
||||
self._set_az_aggregate('only-host2', 'myaz')
|
||||
# Set trait on host2
|
||||
rp_uuid2 = self._get_provider_uuid_by_host('host2')
|
||||
self._set_provider_traits(rp_uuid2, ['HW_CPU_X86_VMX'])
|
||||
# Set trait on aggregate only-host2
|
||||
self._set_traits_on_aggregate('only-host2', ['HW_CPU_X86_VMX'])
|
||||
# Boot the server into that az and make sure we fail
|
||||
server = self._boot_server(
|
||||
flavor_id=self.flavor_with_trait_dxva['id'], az='myaz')
|
||||
self.assertIsNone(self._get_instance_host(server))
|
||||
server = self.api.get_server(server['id'])
|
||||
self.assertEqual('ERROR', server['status'])
|
||||
self.assertIn('No valid host', server['fault']['message'])
|
||||
|
||||
|
||||
class TestAggregateMultiTenancyIsolationFilter(
|
||||
test.TestCase, integrated_helpers.InstanceHelperMixin):
|
||||
|
||||
@@ -30,6 +30,8 @@ class TestRequestFilter(test.NoDBTestCase):
|
||||
group='scheduler')
|
||||
self.flags(query_placement_for_availability_zone=True,
|
||||
group='scheduler')
|
||||
self.flags(enable_isolated_aggregate_filtering=True,
|
||||
group='scheduler')
|
||||
|
||||
def test_process_reqspec(self):
|
||||
fake_filters = [mock.MagicMock(), mock.MagicMock()]
|
||||
@@ -99,6 +101,118 @@ class TestRequestFilter(test.NoDBTestCase):
|
||||
self.assertIn('filter added aggregates', log_lines[0])
|
||||
self.assertIn('took %.1f seconds', log_lines[1])
|
||||
|
||||
@mock.patch('nova.objects.aggregate.AggregateList.'
|
||||
'get_non_matching_by_metadata_keys')
|
||||
def test_isolate_aggregates(self, mock_getnotmd):
|
||||
agg4_traits = {'trait:HW_GPU_API_DXVA': 'required',
|
||||
'trait:HW_NIC_DCB_ETS': 'required'}
|
||||
mock_getnotmd.return_value = [
|
||||
objects.Aggregate(
|
||||
uuid=uuids.agg1,
|
||||
metadata={'trait:CUSTOM_WINDOWS_LICENSED_TRAIT': 'required'}),
|
||||
objects.Aggregate(
|
||||
uuid=uuids.agg2,
|
||||
metadata={'trait:CUSTOM_WINDOWS_LICENSED_TRAIT': 'required',
|
||||
'trait:CUSTOM_XYZ_TRAIT': 'required'}),
|
||||
objects.Aggregate(
|
||||
uuid=uuids.agg4,
|
||||
metadata=agg4_traits),
|
||||
]
|
||||
fake_flavor = objects.Flavor(
|
||||
vcpus=1, memory_mb=1024, root_gb=10, ephemeral_gb=5, swap=0,
|
||||
extra_specs=agg4_traits)
|
||||
fake_image = objects.ImageMeta(
|
||||
properties=objects.ImageMetaProps(
|
||||
traits_required=[]))
|
||||
reqspec = objects.RequestSpec(flavor=fake_flavor, image=fake_image)
|
||||
result = request_filter.isolate_aggregates(self.context, reqspec)
|
||||
self.assertTrue(result)
|
||||
self.assertItemsEqual(
|
||||
set([uuids.agg1, uuids.agg2, uuids.agg4]),
|
||||
reqspec.requested_destination.forbidden_aggregates)
|
||||
mock_getnotmd.assert_called_once_with(self.context, mock.ANY,
|
||||
'trait:', value='required')
|
||||
self.assertItemsEqual(agg4_traits, mock_getnotmd.call_args[0][1])
|
||||
|
||||
@mock.patch('nova.objects.aggregate.AggregateList.'
|
||||
'get_non_matching_by_metadata_keys')
|
||||
def test_isolate_aggregates_union(self, mock_getnotmd):
|
||||
agg_traits = {'trait:HW_GPU_API_DXVA': 'required',
|
||||
'trait:CUSTOM_XYZ_TRAIT': 'required'}
|
||||
mock_getnotmd.return_value = [
|
||||
objects.Aggregate(
|
||||
uuid=uuids.agg2,
|
||||
metadata={'trait:CUSTOM_WINDOWS_LICENSED_TRAIT': 'required',
|
||||
'trait:CUSTOM_XYZ_TRAIT': 'required'}),
|
||||
objects.Aggregate(
|
||||
uuid=uuids.agg4,
|
||||
metadata={'trait:HW_GPU_API_DXVA': 'required',
|
||||
'trait:HW_NIC_DCB_ETS': 'required'}),
|
||||
]
|
||||
fake_flavor = objects.Flavor(
|
||||
vcpus=1, memory_mb=1024, root_gb=10, ephemeral_gb=5, swap=0,
|
||||
extra_specs=agg_traits)
|
||||
fake_image = objects.ImageMeta(
|
||||
properties=objects.ImageMetaProps(
|
||||
traits_required=[]))
|
||||
reqspec = objects.RequestSpec(flavor=fake_flavor, image=fake_image)
|
||||
reqspec.requested_destination = objects.Destination(
|
||||
forbidden_aggregates={uuids.agg1})
|
||||
result = request_filter.isolate_aggregates(self.context, reqspec)
|
||||
self.assertTrue(result)
|
||||
self.assertEqual(
|
||||
','.join(sorted([uuids.agg1, uuids.agg2, uuids.agg4])),
|
||||
','.join(sorted(
|
||||
reqspec.requested_destination.forbidden_aggregates)))
|
||||
mock_getnotmd.assert_called_once_with(self.context, mock.ANY,
|
||||
'trait:', value='required')
|
||||
self.assertItemsEqual(agg_traits, mock_getnotmd.call_args[0][1])
|
||||
|
||||
@mock.patch('nova.objects.aggregate.AggregateList.'
|
||||
'get_non_matching_by_metadata_keys')
|
||||
def test_isolate_agg_trait_on_flavor_destination_not_set(self,
|
||||
mock_getnotmd):
|
||||
mock_getnotmd.return_value = []
|
||||
traits = set(['HW_GPU_API_DXVA', 'HW_NIC_DCB_ETS'])
|
||||
fake_flavor = objects.Flavor(
|
||||
vcpus=1, memory_mb=1024, root_gb=10, ephemeral_gb=5, swap=0,
|
||||
extra_specs={'trait:' + trait: 'required' for trait in traits})
|
||||
fake_image = objects.ImageMeta(
|
||||
properties=objects.ImageMetaProps(
|
||||
traits_required=[]))
|
||||
reqspec = objects.RequestSpec(flavor=fake_flavor, image=fake_image)
|
||||
result = request_filter.isolate_aggregates(self.context, reqspec)
|
||||
self.assertTrue(result)
|
||||
self.assertNotIn('requested_destination', reqspec)
|
||||
keys = ['trait:%s' % trait for trait in traits]
|
||||
mock_getnotmd.assert_called_once_with(self.context, mock.ANY,
|
||||
'trait:', value='required')
|
||||
self.assertItemsEqual(keys, mock_getnotmd.call_args[0][1])
|
||||
|
||||
@mock.patch('nova.objects.aggregate.AggregateList.'
|
||||
'get_non_matching_by_metadata_keys')
|
||||
def test_isolate_agg_trait_on_flv_img_destination_not_set(self,
|
||||
mock_getnotmd):
|
||||
mock_getnotmd.return_value = []
|
||||
flavor_traits = set(['HW_GPU_API_DXVA'])
|
||||
image_traits = set(['HW_NIC_DCB_ETS'])
|
||||
fake_flavor = objects.Flavor(
|
||||
vcpus=1, memory_mb=1024, root_gb=10, ephemeral_gb=5, swap=0,
|
||||
extra_specs={
|
||||
'trait:' + trait: 'required' for trait in flavor_traits})
|
||||
fake_image = objects.ImageMeta(
|
||||
properties=objects.ImageMetaProps(
|
||||
traits_required=[trait for trait in image_traits]))
|
||||
reqspec = objects.RequestSpec(flavor=fake_flavor, image=fake_image)
|
||||
result = request_filter.isolate_aggregates(self.context, reqspec)
|
||||
self.assertTrue(result)
|
||||
self.assertNotIn('requested_destination', reqspec)
|
||||
keys = ['trait:%s' % trait for trait in flavor_traits.union(
|
||||
image_traits)]
|
||||
mock_getnotmd.assert_called_once_with(self.context, mock.ANY,
|
||||
'trait:', value='required')
|
||||
self.assertItemsEqual(keys, mock_getnotmd.call_args[0][1])
|
||||
|
||||
@mock.patch('nova.objects.AggregateList.get_by_metadata')
|
||||
def test_require_tenant_aggregate_no_match(self, getmd):
|
||||
self.flags(placement_aggregate_required_for_tenants=True,
|
||||
@@ -150,9 +264,11 @@ class TestRequestFilter(test.NoDBTestCase):
|
||||
request_filter.map_az_to_placement_aggregate(self.context, reqspec)
|
||||
getmd.assert_not_called()
|
||||
|
||||
@mock.patch('nova.objects.aggregate.AggregateList.'
|
||||
'get_non_matching_by_metadata_keys')
|
||||
@mock.patch('nova.objects.AggregateList.get_by_metadata')
|
||||
def test_with_tenant_and_az(self, getmd):
|
||||
getmd.side_effect = [
|
||||
def test_with_tenant_and_az_and_traits(self, mock_getmd, mock_getnotmd):
|
||||
mock_getmd.side_effect = [
|
||||
# Tenant filter
|
||||
[objects.Aggregate(
|
||||
uuid=uuids.agg1,
|
||||
@@ -168,10 +284,33 @@ class TestRequestFilter(test.NoDBTestCase):
|
||||
uuid=uuids.agg4,
|
||||
metadata={'availability_zone': 'myaz'})],
|
||||
]
|
||||
|
||||
mock_getnotmd.side_effect = [
|
||||
# isolate_aggregates filter
|
||||
[objects.Aggregate(
|
||||
uuid=uuids.agg1,
|
||||
metadata={'trait:CUSTOM_WINDOWS_LICENSED_TRAIT': 'required'}),
|
||||
objects.Aggregate(
|
||||
uuid=uuids.agg2,
|
||||
metadata={'trait:CUSTOM_WINDOWS_LICENSED_TRAIT': 'required',
|
||||
'trait:CUSTOM_XYZ_TRAIT': 'required'}),
|
||||
objects.Aggregate(
|
||||
uuid=uuids.agg3,
|
||||
metadata={'trait:CUSTOM_XYZ_TRAIT': 'required'}),
|
||||
],
|
||||
]
|
||||
|
||||
traits = set(['HW_GPU_API_DXVA', 'HW_NIC_DCB_ETS'])
|
||||
fake_flavor = objects.Flavor(
|
||||
vcpus=1, memory_mb=1024, root_gb=10, ephemeral_gb=5, swap=0,
|
||||
extra_specs={'trait:' + trait: 'required' for trait in traits})
|
||||
fake_image = objects.ImageMeta(
|
||||
properties=objects.ImageMetaProps(
|
||||
traits_required=[]))
|
||||
reqspec = objects.RequestSpec(project_id='owner',
|
||||
availability_zone='myaz')
|
||||
# flavor is needed for the compute_status_filter
|
||||
reqspec.flavor = objects.Flavor(extra_specs={})
|
||||
availability_zone='myaz',
|
||||
flavor=fake_flavor,
|
||||
image=fake_image)
|
||||
request_filter.process_reqspec(self.context, reqspec)
|
||||
self.assertEqual(
|
||||
','.join(sorted([uuids.agg1, uuids.agg2])),
|
||||
@@ -181,12 +320,20 @@ class TestRequestFilter(test.NoDBTestCase):
|
||||
','.join(sorted([uuids.agg4])),
|
||||
','.join(sorted(
|
||||
reqspec.requested_destination.aggregates[1].split(','))))
|
||||
getmd.assert_has_calls([
|
||||
self.assertItemsEqual(
|
||||
set([uuids.agg1, uuids.agg2, uuids.agg3]),
|
||||
reqspec.requested_destination.forbidden_aggregates)
|
||||
mock_getmd.assert_has_calls([
|
||||
mock.call(self.context, value='owner'),
|
||||
mock.call(self.context,
|
||||
key='availability_zone',
|
||||
value='myaz')])
|
||||
|
||||
keys = ['trait:%s' % trait for trait in traits]
|
||||
mock_getnotmd.assert_called_once_with(self.context, mock.ANY,
|
||||
'trait:', value='required')
|
||||
self.assertItemsEqual(keys, mock_getnotmd.call_args[0][1])
|
||||
|
||||
def test_require_image_type_support_disabled(self):
|
||||
self.flags(query_placement_for_image_type_support=False,
|
||||
group='scheduler')
|
||||
|
||||
@@ -95,6 +95,25 @@ class TestUtils(TestUtilsBase):
|
||||
)
|
||||
self._test_resources_from_request_spec(expected_resources, flavor)
|
||||
|
||||
def test_resources_from_request_spec_flavor_req_traits(self):
|
||||
flavor = objects.Flavor(
|
||||
vcpus=1, memory_mb=1024, root_gb=10, ephemeral_gb=5, swap=0,
|
||||
extra_specs={'trait:CUSTOM_FLAVOR_TRAIT': 'required'})
|
||||
expected_resources = FakeResourceRequest()
|
||||
expected_resources._rg_by_id[None] = objects.RequestGroup(
|
||||
use_same_provider=False,
|
||||
resources={
|
||||
'VCPU': 1,
|
||||
'MEMORY_MB': 1024,
|
||||
'DISK_GB': 15,
|
||||
},
|
||||
required_traits=set(['CUSTOM_FLAVOR_TRAIT'])
|
||||
)
|
||||
resources = self._test_resources_from_request_spec(
|
||||
expected_resources, flavor)
|
||||
expected_result = set(['CUSTOM_FLAVOR_TRAIT'])
|
||||
self.assertEqual(expected_result, resources.all_required_traits)
|
||||
|
||||
def test_resources_from_request_spec_flavor_and_image_traits(self):
|
||||
image = self._get_image_with_traits()
|
||||
flavor = objects.Flavor(vcpus=1,
|
||||
|
||||
Reference in New Issue
Block a user