Merge "Nova object changes for forbidden aggregates request filter"

This commit is contained in:
Zuul 2019-09-11 20:11:42 +00:00 committed by Gerrit Code Review
commit 0c2e77a983
9 changed files with 169 additions and 14 deletions

View File

@ -132,7 +132,7 @@ Upgrade
**20.0.0 (Train)**
* Checks for the Placement API are modified to require version 1.31.
* Checks for the Placement API are modified to require version 1.32.
* Checks to ensure block-storage (cinder) API version 3.44 is
available in order to support multi-attach volumes.
If ``[cinder]/auth_type`` is not configured this is a no-op check.

View File

@ -46,11 +46,12 @@ from nova.volume import cinder
CONF = nova.conf.CONF
# NOTE(tetsuro): 1.31 is required by nova-scheduler to use in_tree
# queryparam to get allocation candidates.
# NOTE(vrushali): 1.32 is required by nova-scheduler to use member_of
# queryparam to prepare a list of forbidden aggregates that should be
# ignored by placement service in the allocation candidates API.
# NOTE: If you bump this version, remember to update the history
# section in the nova-status man page (doc/source/cli/nova-status).
MIN_PLACEMENT_MICROVERSION = "1.31"
MIN_PLACEMENT_MICROVERSION = "1.32"
# NOTE(mriedem): 3.44 is needed to work with volume attachment records which
# are required for supporting multi-attach capable volumes.

View File

@ -879,7 +879,8 @@ class Destination(base.NovaObject):
# Version 1.1: Add cell field
# Version 1.2: Add aggregates field
# Version 1.3: Add allow_cross_cell_move field.
VERSION = '1.3'
# Version 1.4: Add forbidden_aggregates field
VERSION = '1.4'
fields = {
'host': fields.StringField(),
@ -897,11 +898,18 @@ class Destination(base.NovaObject):
# scheduler by default selects hosts from the cell specified in the
# cell field.
'allow_cross_cell_move': fields.BooleanField(default=False),
# NOTE(vrushali): These are forbidden aggregates passed to placement as
# query params to the allocation candidates API.
'forbidden_aggregates': fields.SetOfStringsField(nullable=True,
default=None),
}
def obj_make_compatible(self, primitive, target_version):
super(Destination, self).obj_make_compatible(primitive, target_version)
target_version = versionutils.convert_version_to_tuple(target_version)
if target_version < (1, 4):
if 'forbidden_aggregates' in primitive:
del primitive['forbidden_aggregates']
if target_version < (1, 3) and 'allow_cross_cell_move' in primitive:
del primitive['allow_cross_cell_move']
if target_version < (1, 2):
@ -936,6 +944,20 @@ class Destination(base.NovaObject):
self.aggregates = []
self.aggregates.append(','.join(aggregates))
def append_forbidden_aggregates(self, forbidden_aggregates):
"""Add a set of aggregates to the forbidden aggregates.
This will take a set of forbidden aggregates that should be
ignored by the placement service.
:param forbidden_aggregates: A set of aggregates which should be
ignored by the placement service.
"""
if self.forbidden_aggregates is None:
self.forbidden_aggregates = set([])
self.forbidden_aggregates |= forbidden_aggregates
@base.NovaObjectRegistry.register
class SchedulerRetries(base.NovaObject):
@ -1013,7 +1035,8 @@ class RequestGroup(base.NovaObject):
# Version 1.0: Initial version
# Version 1.1: add requester_id and provider_uuids fields
# Version 1.2: add in_tree field
VERSION = '1.2'
# Version 1.3: Add forbidden_aggregates field
VERSION = '1.3'
fields = {
'use_same_provider': fields.BooleanField(default=True),
@ -1027,6 +1050,11 @@ class RequestGroup(base.NovaObject):
# member of the aggregate aggregate_UUID1 and member of the aggregate
# aggregate_UUID2 or aggregate_UUID3 .
'aggregates': fields.ListOfListsOfStringsField(default=[]),
# The forbidden_aggregates field has a form of
# set(['aggregate_UUID1', 'aggregate_UUID12', 'aggregate_UUID3'])
# meaning that the request should not be fulfilled from an RP
# belonging to any of the aggregates in forbidden_aggregates field.
'forbidden_aggregates': fields.SetOfStringsField(default=set()),
# The entity the request is coming from (e.g. the Neutron port uuid)
# which may not always be a UUID.
'requester_id': fields.StringField(nullable=True, default=None),
@ -1079,6 +1107,9 @@ class RequestGroup(base.NovaObject):
super(RequestGroup, self).obj_make_compatible(
primitive, target_version)
target_version = versionutils.convert_version_to_tuple(target_version)
if target_version < (1, 3):
if 'forbidden_aggregates' in primitive:
del primitive['forbidden_aggregates']
if target_version < (1, 2):
if 'in_tree' in primitive:
del primitive['in_tree']

View File

@ -41,9 +41,9 @@ from nova import utils
CONF = nova.conf.CONF
LOG = logging.getLogger(__name__)
WARN_EVERY = 10
NEGATIVE_MEMBER_OF_VERSION = '1.32'
RESHAPER_VERSION = '1.30'
CONSUMER_GENERATION_VERSION = '1.28'
INTREE_AC_VERSION = '1.31'
ALLOW_RESERVED_EQUAL_TOTAL_INVENTORY_VERSION = '1.26'
POST_RPS_RETURNS_PAYLOAD_API_VERSION = '1.20'
AGGREGATE_GENERATION_VERSION = '1.19'
@ -291,7 +291,7 @@ class SchedulerReportClient(object):
"""
# Note that claim_resources() will use this version as well to
# make allocations by `PUT /allocations/{consumer_uuid}`
version = INTREE_AC_VERSION
version = NEGATIVE_MEMBER_OF_VERSION
qparams = resources.to_querystring()
url = "/allocation_candidates?%s" % qparams
resp = self.get(url, version=version,

View File

@ -304,6 +304,7 @@ class ResourceRequest(object):
forbidden_traits = request_group.forbidden_traits
aggregates = request_group.aggregates
in_tree = request_group.in_tree
forbidden_aggregates = request_group.forbidden_aggregates
resource_query = ",".join(
sorted("%s:%s" % (rc, amount)
@ -327,6 +328,12 @@ class ResourceRequest(object):
qs_params.extend(sorted(aggs))
if in_tree:
qs_params.append(('in_tree%s' % suffix, in_tree))
if forbidden_aggregates:
# member_ofN is a list of aggregate uuids. We need a
# tuple of ('member_ofN, '!in:uuid,uuid,...').
forbidden_aggs = '!in:' + ','.join(
sorted(forbidden_aggregates))
qs_params.append(('member_of%s' % suffix, forbidden_aggs))
return qs_params
if self._limit is not None:
@ -479,6 +486,9 @@ def resources_from_request_spec(ctxt, spec_obj, host_manager):
# [['aggA', 'aggB'], ['aggC']]
grp.aggregates = [ored.split(',')
for ored in destination.aggregates]
if destination.forbidden_aggregates:
grp = res_req.get_request_group(None)
grp.forbidden_aggregates |= destination.forbidden_aggregates
if 'force_hosts' in spec_obj and spec_obj.force_hosts:
# Prioritize the value from requested_destination just in case

View File

@ -1049,7 +1049,7 @@ object_data = {
'CpuDiagnostics': '1.0-d256f2e442d1b837735fd17dfe8e3d47',
'DNSDomain': '1.0-7b0b2dab778454b6a7b6c66afe163a1a',
'DNSDomainList': '1.0-4ee0d9efdfd681fed822da88376e04d2',
'Destination': '1.3-07240d223a95c8b9399f7af21091ccfd',
'Destination': '1.4-3b440d29459e2c98987ad5b25ad1cb2c',
'DeviceBus': '1.0-77509ea1ea0dd750d5864b9bd87d3f9d',
'DeviceMetadata': '1.0-04eb8fd218a49cbc3b1e54b774d179f7',
'Diagnostics': '1.0-38ad3e9b1a59306253fc03f97936db95',
@ -1118,7 +1118,7 @@ object_data = {
'PowerVMLiveMigrateData': '1.4-a745f4eda16b45e1bc5686a0c498f27e',
'Quotas': '1.3-40fcefe522111dddd3e5e6155702cf4e',
'QuotasNoOp': '1.3-347a039fc7cfee7b225b68b5181e0733',
'RequestGroup': '1.2-b9f9db748fe8cde0573af69db771c5ce',
'RequestGroup': '1.3-0458d350a8ec9d0673f9be5640a990ce',
'RequestSpec': '1.12-25010470f219af9b6163f2a457a513f5',
'S3ImageMapping': '1.0-7dd7366a890d82660ed121de9092276e',
'SCSIDeviceBus': '1.0-61c1e89a00901069ab1cf2991681533b',

View File

@ -921,6 +921,25 @@ class _TestRequestSpecObject(object):
req_obj.create()
req_obj.save()
def test_destination_forbidden_aggregates_default(self):
destination = objects.Destination()
self.assertIsNone(destination.forbidden_aggregates)
def test_destination_append_forbidden_aggregates(self):
destination = objects.Destination()
destination.append_forbidden_aggregates(set(['foo', 'bar']))
self.assertEqual(
set(['foo', 'bar']), destination.forbidden_aggregates)
destination.append_forbidden_aggregates(set(['bar', 'baz']))
self.assertEqual(
set(['foo', 'bar', 'baz']), destination.forbidden_aggregates)
def test_destination_delete_forbidden_aggregates(self):
destination = objects.Destination()
destination.append_forbidden_aggregates(set(['foo']))
primitive = destination.obj_to_primitive(target_version='1.0')
self.assertNotIn('forbidden_aggregates', primitive['nova_object.data'])
class TestRequestSpecObject(test_objects._LocalTest,
_TestRequestSpecObject):
@ -997,11 +1016,23 @@ class TestRequestGroupObject(test.NoDBTestCase):
def test_compat_requester_and_provider(self):
req_obj = objects.RequestGroup(
requester_id=uuids.requester, provider_uuids=[uuids.rp1],
required_traits=set(['CUSTOM_PHYSNET_2']))
required_traits=set(['CUSTOM_PHYSNET_2']),
forbidden_aggregates=set(['agg3', 'agg4']))
versions = ovo_base.obj_tree_get_versions('RequestGroup')
primitive = req_obj.obj_to_primitive(
target_version='1.3',
version_manifest=versions)['nova_object.data']
self.assertIn('forbidden_aggregates', primitive)
self.assertIn('in_tree', primitive)
self.assertIn('requester_id', primitive)
self.assertIn('provider_uuids', primitive)
self.assertIn('required_traits', primitive)
self.assertItemsEqual(
primitive['forbidden_aggregates'], set(['agg3', 'agg4']))
primitive = req_obj.obj_to_primitive(
target_version='1.2',
version_manifest=versions)['nova_object.data']
self.assertNotIn('forbidden_aggregates', primitive)
self.assertIn('in_tree', primitive)
self.assertIn('requester_id', primitive)
self.assertIn('provider_uuids', primitive)
@ -1009,6 +1040,7 @@ class TestRequestGroupObject(test.NoDBTestCase):
primitive = req_obj.obj_to_primitive(
target_version='1.1',
version_manifest=versions)['nova_object.data']
self.assertNotIn('forbidden_aggregates', primitive)
self.assertNotIn('in_tree', primitive)
self.assertIn('requester_id', primitive)
self.assertIn('provider_uuids', primitive)
@ -1016,12 +1048,47 @@ class TestRequestGroupObject(test.NoDBTestCase):
primitive = req_obj.obj_to_primitive(
target_version='1.0',
version_manifest=versions)['nova_object.data']
self.assertNotIn('forbidden_aggregates', primitive)
self.assertNotIn('in_tree', primitive)
self.assertNotIn('requester_id', primitive)
self.assertNotIn('provider_uuids', primitive)
self.assertIn('required_traits', primitive)
class TestDestinationObject(test.NoDBTestCase):
def setUp(self):
super(TestDestinationObject, self).setUp()
self.user_id = uuids.user_id
self.project_id = uuids.project_id
self.context = context.RequestContext(uuids.user_id, uuids.project_id)
def test_obj_make_compatible_destination(self):
values = {
'host': 'fake_host',
'node': 'fake_node',
'aggregates': ['agg1', 'agg2'],
'forbidden_aggregates': set(['agg3', 'agg4'])}
obj = objects.Destination(self.context, **values)
data = lambda x: x['nova_object.data']
obj_primitive = data(obj.obj_to_primitive(target_version='1.3'))
self.assertNotIn('forbidden_aggregates', obj_primitive)
self.assertIn('aggregates', obj_primitive)
def test_obj_make_compatible_destination_with_forbidden_aggregates(self):
values = {
'host': 'fake_host',
'node': 'fake_node',
'aggregates': ['agg1', 'agg2'],
'forbidden_aggregates': set(['agg3', 'agg4'])}
obj = objects.Destination(self.context, **values)
data = lambda x: x['nova_object.data']
obj_primitive = data(obj.obj_to_primitive(target_version='1.4'))
self.assertIn('forbidden_aggregates', obj_primitive)
self.assertItemsEqual(obj_primitive['forbidden_aggregates'],
set(['agg3', 'agg4']))
self.assertIn('aggregates', obj_primitive)
class TestMappingRequestGroupsToProviders(test.NoDBTestCase):
def setUp(self):
super(TestMappingRequestGroupsToProviders, self).setUp()

View File

@ -2085,10 +2085,13 @@ class TestProviderOperations(SchedulerReportClientTestCase):
resources = scheduler_utils.ResourceRequest(req_spec)
resources.get_request_group(None).aggregates = [
['agg1', 'agg2', 'agg3'], ['agg1', 'agg2']]
forbidden_aggs = set(['agg1', 'agg5', 'agg6'])
resources.get_request_group(None).forbidden_aggregates = forbidden_aggs
expected_path = '/allocation_candidates'
expected_query = [
('group_policy', 'isolate'),
('limit', '1000'),
('member_of', '!in:agg1,agg5,agg6'),
('member_of', 'in:agg1,agg2'),
('member_of', 'in:agg1,agg2,agg3'),
('required', 'CUSTOM_TRAIT1,HW_CPU_X86_AVX,!CUSTOM_TRAIT3,'
@ -2115,7 +2118,7 @@ class TestProviderOperations(SchedulerReportClientTestCase):
expected_url = '/allocation_candidates?%s' % parse.urlencode(
expected_query)
self.ks_adap_mock.get.assert_called_once_with(
expected_url, microversion='1.31',
expected_url, microversion='1.32',
global_request_id=self.context.global_id)
self.assertEqual(mock.sentinel.alloc_reqs, alloc_reqs)
self.assertEqual(mock.sentinel.p_sums, p_sums)
@ -2159,7 +2162,7 @@ class TestProviderOperations(SchedulerReportClientTestCase):
expected_query)
self.assertEqual(mock.sentinel.alloc_reqs, alloc_reqs)
self.ks_adap_mock.get.assert_called_once_with(
expected_url, microversion='1.31',
expected_url, microversion='1.32',
global_request_id=self.context.global_id)
self.assertEqual(mock.sentinel.p_sums, p_sums)
@ -2185,7 +2188,7 @@ class TestProviderOperations(SchedulerReportClientTestCase):
res = self.client.get_allocation_candidates(self.context, resources)
self.ks_adap_mock.get.assert_called_once_with(
mock.ANY, microversion='1.31',
mock.ANY, microversion='1.32',
global_request_id=self.context.global_id)
url = self.ks_adap_mock.get.call_args[0][0]
split_url = parse.urlsplit(url)

View File

@ -410,6 +410,49 @@ class TestUtils(TestUtilsBase):
self.context, reqspec, self.mock_host_manager)
self.assertEqual([], req.get_request_group(None).aggregates)
def test_resources_from_request_spec_forbidden_aggregates(self):
flavor = objects.Flavor(vcpus=1, memory_mb=1024,
root_gb=1, ephemeral_gb=0,
swap=0)
reqspec = objects.RequestSpec(
flavor=flavor,
requested_destination=objects.Destination(
forbidden_aggregates=set(['foo', 'bar'])))
req = utils.resources_from_request_spec(self.context, reqspec,
self.mock_host_manager)
self.assertEqual(set(['foo', 'bar']),
req.get_request_group(None).forbidden_aggregates)
def test_resources_from_request_spec_no_forbidden_aggregates(self):
flavor = objects.Flavor(vcpus=1, memory_mb=1024,
root_gb=1, ephemeral_gb=0,
swap=0)
reqspec = objects.RequestSpec(flavor=flavor)
req = utils.resources_from_request_spec(
self.context, reqspec, self.mock_host_manager)
self.assertEqual(set([]), req.get_request_group(None).
forbidden_aggregates)
reqspec.requested_destination = None
req = utils.resources_from_request_spec(
self.context, reqspec, self.mock_host_manager)
self.assertEqual(set([]), req.get_request_group(None).
forbidden_aggregates)
reqspec.requested_destination = objects.Destination()
req = utils.resources_from_request_spec(
self.context, reqspec, self.mock_host_manager)
self.assertEqual(set([]), req.get_request_group(None).
forbidden_aggregates)
reqspec.requested_destination.forbidden_aggregates = None
req = utils.resources_from_request_spec(
self.context, reqspec, self.mock_host_manager)
self.assertEqual(set([]), req.get_request_group(None).
forbidden_aggregates)
def test_process_extra_specs_granular_called(self):
flavor = objects.Flavor(vcpus=1,
memory_mb=1024,