diff --git a/doc/source/cli/nova-status.rst b/doc/source/cli/nova-status.rst index c65541ba5345..dbd42991148b 100644 --- a/doc/source/cli/nova-status.rst +++ b/doc/source/cli/nova-status.rst @@ -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. diff --git a/nova/cmd/status.py b/nova/cmd/status.py index 87ddfc1ce6e3..899e623748cf 100644 --- a/nova/cmd/status.py +++ b/nova/cmd/status.py @@ -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. diff --git a/nova/objects/request_spec.py b/nova/objects/request_spec.py index 56f59e9f91e4..01c34df16dc1 100644 --- a/nova/objects/request_spec.py +++ b/nova/objects/request_spec.py @@ -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'] diff --git a/nova/scheduler/client/report.py b/nova/scheduler/client/report.py index 73e50917a65d..9477f5fa0ca8 100644 --- a/nova/scheduler/client/report.py +++ b/nova/scheduler/client/report.py @@ -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, diff --git a/nova/scheduler/utils.py b/nova/scheduler/utils.py index 9ed4878289de..0ffa54003bd2 100644 --- a/nova/scheduler/utils.py +++ b/nova/scheduler/utils.py @@ -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 diff --git a/nova/tests/unit/objects/test_objects.py b/nova/tests/unit/objects/test_objects.py index 465942a8e4b3..170cd8a458f5 100644 --- a/nova/tests/unit/objects/test_objects.py +++ b/nova/tests/unit/objects/test_objects.py @@ -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', diff --git a/nova/tests/unit/objects/test_request_spec.py b/nova/tests/unit/objects/test_request_spec.py index ccec13863493..d8f1dba2fcd2 100644 --- a/nova/tests/unit/objects/test_request_spec.py +++ b/nova/tests/unit/objects/test_request_spec.py @@ -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() diff --git a/nova/tests/unit/scheduler/client/test_report.py b/nova/tests/unit/scheduler/client/test_report.py index 396a817147bf..48a196d30bfe 100644 --- a/nova/tests/unit/scheduler/client/test_report.py +++ b/nova/tests/unit/scheduler/client/test_report.py @@ -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) diff --git a/nova/tests/unit/scheduler/test_utils.py b/nova/tests/unit/scheduler/test_utils.py index 0d048456dbac..44c981339232 100644 --- a/nova/tests/unit/scheduler/test_utils.py +++ b/nova/tests/unit/scheduler/test_utils.py @@ -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,