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:
parent
688f93d59f
commit
a9e7581f00
|
@ -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
|
||||
========
|
||||
|
||||
|
|
|
@ -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.') %
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in New Issue