diff --git a/doc/source/cli/nova-status.rst b/doc/source/cli/nova-status.rst index ef7ca2905dd9..56e60adef81d 100644 --- a/doc/source/cli/nova-status.rst +++ b/doc/source/cli/nova-status.rst @@ -104,6 +104,10 @@ Upgrade * Checks for the Placement API are modified to require version 1.17. + **18.0.0 (Rocky)** + + * Checks for the Placement API are modified to require version 1.21. + See Also ======== diff --git a/nova/cmd/status.py b/nova/cmd/status.py index 2a0e742c2bb9..782527460103 100644 --- a/nova/cmd/status.py +++ b/nova/cmd/status.py @@ -196,11 +196,11 @@ class UpgradeCommands(object): versions = self._placement_get("/") max_version = pkg_resources.parse_version( versions["versions"][0]["max_version"]) - # NOTE(mriedem): 1.17 is required by nova-scheduler to get - # allocation candidates with required traits from the flavor. + # NOTE(mriedem): 1.21 is required by nova-scheduler to be able + # to request aggregates. # NOTE: If you bump this version, remember to update the history # section in the nova-status man page (doc/source/cli/nova-status). - needs_version = pkg_resources.parse_version("1.17") + needs_version = pkg_resources.parse_version("1.21") if max_version < needs_version: msg = (_('Placement API version %(needed)s needed, ' 'you have %(current)s.') % diff --git a/nova/scheduler/client/report.py b/nova/scheduler/client/report.py index d3ba15223e0b..21ae8592fea4 100644 --- a/nova/scheduler/client/report.py +++ b/nova/scheduler/client/report.py @@ -329,8 +329,17 @@ class SchedulerReportClient(object): :param context: The security context :param nova.scheduler.utils.ResourceRequest resources: - A ResourceRequest object representing the requested resources and - traits from the request spec. + A ResourceRequest object representing the requested resources, + traits, and aggregates from the request spec. + + Example member_of (aggregates) value in resources: + + [('foo', 'bar'), ('baz',)] + + translates to: + + "Candidates are in either 'foo' or 'bar', but definitely in 'baz'" + """ # TODO(efried): For now, just use the unnumbered group to retain # existing behavior. Once the GET /allocation_candidates API is @@ -338,6 +347,7 @@ class SchedulerReportClient(object): # and traits in the query string (via a new method on ResourceRequest). res = resources.get_request_group(None).resources required_traits = resources.get_request_group(None).required_traits + aggregates = resources.get_request_group(None).member_of resource_query = ",".join( sorted("%s:%s" % (rc, amount) @@ -348,8 +358,19 @@ class SchedulerReportClient(object): } if required_traits: qs_params['required'] = ",".join(required_traits) + if aggregates: + # NOTE(danms): In 1.21, placement cannot take an AND'd + # set of aggregates, only an OR'd set. Thus, if we have + # required and optional sets, we must do the naive thing + # and AND ours together. That will not achieve the same + # result, but we can't do it from the client side. When + # placement supports AND'ing multiple sets, we can fix this. + # TODO(danms): Update this when placement can take multiple + # member_of query parameters. + required_agg = set.intersection(*[set(x) for x in aggregates]) + qs_params['member_of'] = 'in:' + ','.join(sorted(required_agg)) - version = '1.17' + version = '1.21' url = "/allocation_candidates?%s" % parse.urlencode(qs_params) resp = self.get(url, version=version, global_request_id=context.global_id) @@ -363,15 +384,12 @@ class SchedulerReportClient(object): 'status_code': resp.status_code, 'err_text': resp.text, } - if required_traits: - msg = ("Failed to retrieve allocation candidates from placement " - "API for filters %(resources)s and traits %(traits)s. Got " - "%(status_code)d: %(err_text)s.") - args['traits'] = qs_params['required'] - else: - msg = ("Failed to retrieve allocation candidates from placement " - "API for filters %(resources)s. Got %(status_code)d: " - "%(err_text)s.") + msg = ("Failed to retrieve allocation candidates from placement " + "API for filters %(resources)s, traits %(traits)s, " + "aggregates %(aggregates)s. Got " + "%(status_code)d: %(err_text)s.") + args['traits'] = qs_params.get('required', '(none)') + args['aggregates'] = qs_params.get('aggregates', '(none)') LOG.error(msg, args) return None, None, None diff --git a/nova/scheduler/utils.py b/nova/scheduler/utils.py index 8be3db00bf80..6e5d0b9d17bd 100644 --- a/nova/scheduler/utils.py +++ b/nova/scheduler/utils.py @@ -285,8 +285,8 @@ def merge_resources(original_resources, new_resources, sign=1): def resources_from_request_spec(spec_obj): - """Given a RequestSpec object, returns a ResourceRequest of the resources - and traits it represents. + """Given a RequestSpec object, returns a ResourceRequest of the resources, + traits, and aggregates it represents. """ spec_resources = { fields.ResourceClass.VCPU: spec_obj.vcpus, @@ -333,6 +333,13 @@ def resources_from_request_spec(spec_obj): for rclass, amount in spec_resources.items(): res_req.get_request_group(None).resources[rclass] = amount + if 'requested_destination' in spec_obj: + destination = spec_obj.requested_destination + if destination and destination.aggregates: + grp = res_req.get_request_group(None) + grp.member_of = [tuple(ored.split(',')) + for ored in destination.aggregates] + return res_req diff --git a/nova/tests/unit/cmd/test_status.py b/nova/tests/unit/cmd/test_status.py index 552362af39ee..e61076439bab 100644 --- a/nova/tests/unit/cmd/test_status.py +++ b/nova/tests/unit/cmd/test_status.py @@ -210,7 +210,7 @@ class TestPlacementCheck(test.NoDBTestCase): "versions": [ { "min_version": "1.0", - "max_version": "1.17", + "max_version": "1.21", "id": "v1.0" } ] @@ -230,7 +230,7 @@ class TestPlacementCheck(test.NoDBTestCase): "versions": [ { "min_version": "1.0", - "max_version": "1.17", + "max_version": "1.21", "id": "v1.0" } ] @@ -251,7 +251,7 @@ class TestPlacementCheck(test.NoDBTestCase): } res = self.cmd._check_placement() self.assertEqual(status.UpgradeCheckCode.FAILURE, res.code) - self.assertIn('Placement API version 1.17 needed, you have 0.9', + self.assertIn('Placement API version 1.21 needed, you have 0.9', res.details) diff --git a/nova/tests/unit/scheduler/client/test_report.py b/nova/tests/unit/scheduler/client/test_report.py index 5bf3108bef51..c9aaca80b88a 100644 --- a/nova/tests/unit/scheduler/client/test_report.py +++ b/nova/tests/unit/scheduler/client/test_report.py @@ -1520,9 +1520,12 @@ class TestProviderOperations(SchedulerReportClientTestCase): 'trait:CUSTOM_TRAIT1': 'required', 'trait:CUSTOM_TRAIT2': 'preferred', }) + resources.get_request_group(None).member_of = [ + ('agg1', 'agg2', 'agg3'), ('agg1', 'agg2')] expected_path = '/allocation_candidates' expected_query = {'resources': ['MEMORY_MB:1024,VCPU:1'], 'required': ['CUSTOM_TRAIT1'], + 'member_of': ['in:agg1,agg2'], 'limit': ['1000']} resp_mock.json.return_value = json_data @@ -1532,7 +1535,7 @@ class TestProviderOperations(SchedulerReportClientTestCase): self.client.get_allocation_candidates(self.context, resources) self.ks_adap_mock.get.assert_called_once_with( - mock.ANY, raise_exc=False, microversion='1.17', + mock.ANY, raise_exc=False, microversion='1.21', headers={'X-Openstack-Request-Id': self.context.global_id}) url = self.ks_adap_mock.get.call_args[0][0] split_url = parse.urlsplit(url) @@ -1564,7 +1567,7 @@ class TestProviderOperations(SchedulerReportClientTestCase): self.client.get_allocation_candidates(self.context, resources) self.ks_adap_mock.get.assert_called_once_with( - mock.ANY, raise_exc=False, microversion='1.17', + mock.ANY, raise_exc=False, microversion='1.21', headers={'X-Openstack-Request-Id': self.context.global_id}) url = self.ks_adap_mock.get.call_args[0][0] split_url = parse.urlsplit(url) @@ -1591,7 +1594,7 @@ class TestProviderOperations(SchedulerReportClientTestCase): res = self.client.get_allocation_candidates(self.context, resources) self.ks_adap_mock.get.assert_called_once_with( - mock.ANY, raise_exc=False, microversion='1.17', + mock.ANY, raise_exc=False, microversion='1.21', headers={'X-Openstack-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 f4202df32651..39117ff48d03 100644 --- a/nova/tests/unit/scheduler/test_utils.py +++ b/nova/tests/unit/scheduler/test_utils.py @@ -230,6 +230,45 @@ class TestUtils(test.NoDBTestCase): ) self._test_resources_from_request_spec(flavor, expected_resources) + def test_resources_from_request_spec_aggregates(self): + destination = objects.Destination() + flavor = objects.Flavor(vcpus=1, memory_mb=1024, + root_gb=1, ephemeral_gb=0, + swap=0) + reqspec = objects.RequestSpec(flavor=flavor, + requested_destination=destination) + + destination.require_aggregates(['foo', 'bar']) + req = utils.resources_from_request_spec(reqspec) + self.assertEqual([('foo', 'bar',)], + req.get_request_group(None).member_of) + + destination.require_aggregates(['baz']) + req = utils.resources_from_request_spec(reqspec) + self.assertEqual([('foo', 'bar'), ('baz',)], + req.get_request_group(None).member_of) + + def test_resources_from_request_spec_no_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(reqspec) + self.assertEqual([], req.get_request_group(None).member_of) + + reqspec.requested_destination = None + req = utils.resources_from_request_spec(reqspec) + self.assertEqual([], req.get_request_group(None).member_of) + + reqspec.requested_destination = objects.Destination() + req = utils.resources_from_request_spec(reqspec) + self.assertEqual([], req.get_request_group(None).member_of) + + reqspec.requested_destination.aggregates = None + req = utils.resources_from_request_spec(reqspec) + self.assertEqual([], req.get_request_group(None).member_of) + @mock.patch("nova.scheduler.utils.ResourceRequest.from_extra_specs") def test_process_extra_specs_granular_called(self, mock_proc): flavor = objects.Flavor(vcpus=1,