Make get_allocation_candidates() honor aggregate restrictions

This uses the member_of query parameter to placement to ensure that the
candidates returned are within the appropriate aggregate(s) if so
specified.

Related to blueprint placement-req-filter

Change-Id: If8ac06039ac9d647efdc088fbe944938e205e941
This commit is contained in:
Dan Smith 2018-02-26 02:10:06 -08:00
parent 688f93d59f
commit a9e7581f00
7 changed files with 94 additions and 23 deletions

View File

@ -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
========

View File

@ -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.') %

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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,