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:
shilpa
2019-09-12 16:56:32 -05:00
committed by Eric Fried
parent e7000b1f7d
commit 1db3b89cae
5 changed files with 578 additions and 12 deletions
+11
View File
@@ -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",
+44
View File
@@ -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,
]
+351 -6
View File
@@ -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')
+19
View File
@@ -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,