Merge "Nova object changes for forbidden aggregates request filter"
This commit is contained in:
commit
0c2e77a983
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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']
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in New Issue