Merge "Support granular allocation candidate list"

This commit is contained in:
Zuul 2021-03-19 15:42:20 +00:00 committed by Gerrit Code Review
commit d6ebc8c543
5 changed files with 286 additions and 56 deletions

View File

@ -11,6 +11,7 @@
# under the License.
import argparse
import collections
from osc_lib.command import command
from osc_lib import exceptions
@ -21,6 +22,23 @@ from osc_placement import version
BASE_URL = '/allocation_candidates'
class GroupAction(argparse.Action):
def __call__(self, parser, namespace, values, option_string=None):
group, = values
namespace._current_group = group
groups = namespace.__dict__.setdefault('groups', {})
groups[group] = collections.defaultdict(list)
class AppendToGroup(argparse.Action):
def __call__(self, parser, namespace, values, option_string=None):
if getattr(namespace, '_current_group', None) is None:
groups = namespace.__dict__.setdefault('groups', {})
namespace._current_group = ''
groups[''] = collections.defaultdict(list)
namespace.groups[namespace._current_group][self.dest].append(values)
class ListAllocationCandidate(command.Lister, version.CheckerMixin):
"""List allocation candidates.
@ -60,8 +78,7 @@ class ListAllocationCandidate(command.Lister, version.CheckerMixin):
'--resource',
metavar='<resource_class>=<value>',
dest='resources',
action='append',
default=[],
action=AppendToGroup,
help='String indicating an amount of resource of a specified '
'class that providers in each allocation request must '
'collectively have the capacity and availability to serve. '
@ -81,8 +98,7 @@ class ListAllocationCandidate(command.Lister, version.CheckerMixin):
parser.add_argument(
'--required',
metavar='<required>',
action='append',
default=[],
action=AppendToGroup,
help='A required trait. May be repeated. Allocation candidates '
'must collectively contain all of the required traits. '
'This option requires at least '
@ -91,8 +107,7 @@ class ListAllocationCandidate(command.Lister, version.CheckerMixin):
parser.add_argument(
'--forbidden',
metavar='<forbidden>',
action='append',
default=[],
action=AppendToGroup,
help='A forbidden trait. May be repeated. Returned allocation '
'candidates must not contain any of the specified traits. '
'This option requires at least '
@ -103,8 +118,7 @@ class ListAllocationCandidate(command.Lister, version.CheckerMixin):
aggregate_group = parser.add_mutually_exclusive_group()
aggregate_group.add_argument(
"--member-of",
default=[],
action='append',
action=AppendToGroup,
metavar='<member_of>',
help='A list of comma-separated UUIDs of the resource provider '
'aggregates. The returned allocation candidates must be '
@ -119,56 +133,101 @@ class ListAllocationCandidate(command.Lister, version.CheckerMixin):
)
aggregate_group.add_argument(
'--aggregate-uuid',
default=[],
action='append',
action=AppendToGroup,
metavar='<aggregate_uuid>',
help=argparse.SUPPRESS
)
parser.add_argument(
'--group',
action=GroupAction,
metavar='<group>',
help='An integer to group granular requests. If specified, '
'following given options of resources, required/forbidden '
'traits, and aggregate are associated to that group and will '
'be satisfied by the same resource provider in the response. '
'Can be repeated to get candidates from multiple resource '
'providers in the same resource provider tree. '
'For example, ``--group 1 --resource VCPU=3 --required '
'HW_CPU_X86_AVX --group 2 --resource VCPU=2 --required '
'HW_CPU_X86_SSE`` will provide candidates where three VCPUs '
'comes from a provider with ``HW_CPU_X86_AVX`` trait and '
'two VCPUs from a provider with ``HW_CPU_X86_SSE`` trait. '
'This option requires at least '
'``--os-placement-api-version 1.25`` or greater, but to have '
'placement server be aware of resource provider tree, use '
'``--os-placement-api-version 1.29`` or greater.'
)
parser.add_argument(
'--group-policy',
choices=['none', 'isolate'],
default='none',
metavar='<group_policy>',
help='This indicates how the groups should interact when multiple '
'groups are supplied. With group_policy=none (default), '
'separate groups may or may not be satisfied by the same '
'provider. With group_policy=isolate, numbered groups are '
'guaranteed to be satisfied by different providers.'
)
return parser
@version.check(version.ge('1.10'))
def take_action(self, parsed_args):
if not parsed_args.resources:
http = self.app.client_manager.placement
params = {}
if 'groups' not in parsed_args:
raise exceptions.CommandError(
'At least one --resource must be specified.')
for resource in parsed_args.resources:
if not len(resource.split('=')) == 2:
raise exceptions.CommandError(
'Arguments to --resource must be of form '
'<resource_class>=<value>')
http = self.app.client_manager.placement
params = {'resources': ','.join(
resource.replace('=', ':') for resource in parsed_args.resources)}
if 'limit' in parsed_args and parsed_args.limit:
# Fail if --limit but not high enough microversion.
self.check_version(version.ge('1.16'))
params['limit'] = int(parsed_args.limit)
if 'required' in parsed_args and parsed_args.required:
# Fail if --required but not high enough microversion.
self.check_version(version.ge('1.17'))
params['required'] = ','.join(parsed_args.required)
if 'forbidden' in parsed_args and parsed_args.forbidden:
self.check_version(version.ge('1.22'))
forbidden_traits = ','.join(
['!' + f for f in parsed_args.forbidden])
if 'required' in params:
params['required'] += ',' + forbidden_traits
else:
params['required'] = forbidden_traits
if 'aggregate_uuid' in parsed_args and parsed_args.aggregate_uuid:
# Fail if --aggregate_uuid but not high enough microversion.
self.check_version(version.ge('1.21'))
self.deprecated_option_warning("--aggregate-uuid", "--member-of")
params['member_of'] = 'in:' + ','.join(parsed_args.aggregate_uuid)
if 'member_of' in parsed_args and parsed_args.member_of:
# Fail if --member-of but not high enough microversion.
self.check_version(version.ge('1.21'))
params['member_of'] = [
'in:' + aggs for aggs in parsed_args.member_of]
if any(parsed_args.groups):
self.check_version(version.ge('1.25'))
params['group_policy'] = parsed_args.group_policy
for suffix, group in parsed_args.groups.items():
def _get_key(name):
return name + suffix
if 'resources' not in group:
raise exceptions.CommandError(
'--resources should be provided in group %s', suffix)
for resource in group['resources']:
if not len(resource.split('=')) == 2:
raise exceptions.CommandError(
'Arguments to --resource must be of form '
'<resource_class>=<value>')
params[_get_key('resources')] = ','.join(
resource.replace('=', ':') for resource in group['resources'])
if 'required' in group and group['required']:
# Fail if --required but not high enough microversion.
self.check_version(version.ge('1.17'))
params[_get_key('required')] = ','.join(group['required'])
if 'forbidden' in group and group['forbidden']:
self.check_version(version.ge('1.22'))
forbidden_traits = ','.join(
['!' + f for f in group['forbidden']])
if 'required' in params:
params[_get_key('required')] += ',' + forbidden_traits
else:
params[_get_key('required')] = forbidden_traits
if 'aggregate_uuid' in group and group['aggregate_uuid']:
# Fail if --aggregate_uuid but not high enough microversion.
self.check_version(version.ge('1.21'))
self.deprecated_option_warning(
"--aggregate-uuid", "--member-of")
params[_get_key('member_of')] = 'in:' + ','.join(
group['aggregate_uuid'])
if 'member_of' in group and group['member_of']:
# Fail if --member-of but not high enough microversion.
self.check_version(version.ge('1.21'))
params[_get_key('member_of')] = [
'in:' + aggs for aggs in group['member_of']]
resp = http.request('GET', BASE_URL, params=params).json()

View File

@ -409,23 +409,44 @@ class BaseTestCase(base.BaseTestCase):
cmd = 'resource provider trait delete %s ' % uuid
self.openstack(cmd)
def allocation_candidate_list(self, resources, required=None,
def allocation_candidate_list(self, resources=None, required=None,
forbidden=None, limit=None,
aggregate_uuids=None, member_of=None,
may_print_to_stderr=False):
cmd = 'allocation candidate list ' + ' '.join(
'--resource %s' % resource for resource in resources)
if required is not None:
cmd += ''.join([' --required %s' % t for t in required])
if forbidden:
cmd += ' ' + ' '.join('--forbidden %s' % f for f in forbidden)
cmd = 'allocation candidate list '
cmd += self._allocation_candidates_option(
resources, required, forbidden, aggregate_uuids, member_of)
if limit is not None:
cmd += ' --limit %d' % limit
if aggregate_uuids:
cmd += ' ' + ' '.join(
'--aggregate-uuid %s' % a for a in aggregate_uuids)
if member_of:
cmd += ' ' + ' '.join(
['--member-of %s' % m for m in member_of])
return self.openstack(
cmd, use_json=True, may_print_to_stderr=may_print_to_stderr)
def allocation_candidate_granular(self, groups, group_policy=None,
limit=None):
cmd = 'allocation candidate list '
for suffix, req_group in groups.items():
cmd += ' --group %s' % suffix
cmd += self._allocation_candidates_option(**req_group)
if limit is not None:
cmd += ' --limit %d' % limit
if group_policy is not None:
cmd += ' --group-policy %s' % group_policy
return self.openstack(cmd, use_json=True)
def _allocation_candidates_option(self, resources=None, required=None,
forbidden=None, aggregate_uuids=None,
member_of=None):
opt = ''
if resources:
opt += ' ' + ' '.join(
'--resource %s' % resource for resource in resources)
if required is not None:
opt += ''.join([' --required %s' % t for t in required])
if forbidden:
opt += ' ' + ' '.join('--forbidden %s' % f for f in forbidden)
if aggregate_uuids:
opt += ' ' + ' '.join(
'--aggregate-uuid %s' % a for a in aggregate_uuids)
if member_of:
opt += ' ' + ' '.join(['--member-of %s' % m for m in member_of])
return opt

View File

@ -301,3 +301,122 @@ class TestAllocationCandidate124(base.BaseTestCase):
candidate_dict = {rp['resource provider']: rp for rp in rps}
self.assertEqual(1, len(candidate_dict))
self.assertIn(rp2['uuid'], candidate_dict)
def test_fail_granular_wrong_version(self):
groups = {'1': {'resources': ('VCPU=3',)}}
self.assertCommandFailed(
'Operation or argument is not supported with version 1.24',
self.allocation_candidate_granular, groups=groups)
class TestAllocationCandidate129(base.BaseTestCase):
VERSION = '1.29'
def setUp(self):
super(TestAllocationCandidate129, self).setUp()
self.rp1 = self.resource_provider_create()
self.rp1_1 = self.resource_provider_create(
parent_provider_uuid=self.rp1['uuid'])
self.rp1_2 = self.resource_provider_create(
parent_provider_uuid=self.rp1['uuid'])
self.agg1 = str(uuid.uuid4())
self.agg2 = str(uuid.uuid4())
self.resource_provider_aggregate_set(
self.rp1_1['uuid'], self.agg1, generation=0)
self.resource_provider_aggregate_set(
self.rp1_2['uuid'], self.agg2, generation=0)
self.resource_inventory_set(self.rp1['uuid'], 'DISK_GB=512')
self.resource_inventory_set(
self.rp1_1['uuid'], 'VCPU=8', 'MEMORY_MB=8192')
self.resource_inventory_set(
self.rp1_2['uuid'], 'VCPU=16', 'MEMORY_MB=8192')
self.resource_provider_trait_set(self.rp1_1['uuid'], 'HW_CPU_X86_AVX')
self.resource_provider_trait_set(self.rp1_2['uuid'], 'HW_CPU_X86_SSE')
def test_granular_one_group(self):
groups = {
'1': {'resources': ('VCPU=3',)}
}
rows = self.allocation_candidate_granular(groups=groups)
self.assertEqual(2, len(rows))
numbers = {row['#'] for row in rows}
self.assertEqual(2, len(numbers))
rps = {row['resource provider'] for row in rows}
self.assertEqual(2, len(rps))
self.assertIn(self.rp1_1['uuid'], rps)
self.assertIn(self.rp1_2['uuid'], rps)
def test_granular_two_groups(self):
groups = {
'1': {'resources': ('VCPU=3',)},
'2': {'resources': ('VCPU=3',)}
}
rows = self.allocation_candidate_granular(groups=groups)
self.assertEqual(6, len(rows))
numbers = {row['#'] for row in rows}
self.assertEqual(4, len(numbers))
rps = {row['resource provider'] for row in rows}
self.assertEqual(2, len(rps))
self.assertIn(self.rp1_1['uuid'], rps)
self.assertIn(self.rp1_2['uuid'], rps)
rows = self.allocation_candidate_granular(groups=groups,
group_policy='isolate')
self.assertEqual(4, len(rows))
numbers = {row['#'] for row in rows}
self.assertEqual(2, len(numbers))
rps = {row['resource provider'] for row in rows}
self.assertEqual(2, len(rps))
self.assertIn(self.rp1_1['uuid'], rps)
self.assertIn(self.rp1_2['uuid'], rps)
rows = self.allocation_candidate_granular(groups=groups,
group_policy='isolate',
limit=1)
self.assertEqual(2, len(rows))
numbers = {row['#'] for row in rows}
self.assertEqual(1, len(numbers))
rps = {row['resource provider'] for row in rows}
self.assertEqual(2, len(rps))
self.assertIn(self.rp1_1['uuid'], rps)
self.assertIn(self.rp1_2['uuid'], rps)
def test_granular_traits1(self):
groups = {
'1': {'resources': ('VCPU=6',)},
'2': {'resources': ('VCPU=10',),
'required': ['HW_CPU_X86_AVX']}
}
rows = self.allocation_candidate_granular(groups=groups,
group_policy='isolate')
self.assertEqual(0, len(rows))
def test_granular_traits2(self):
groups = {
'1': {'resources': ('VCPU=6',)},
'2': {'resources': ('VCPU=10',),
'required': ['HW_CPU_X86_SSE']}
}
rows = self.allocation_candidate_granular(groups=groups,
group_policy='isolate')
self.assertEqual(2, len(rows))
numbers = {row['#'] for row in rows}
self.assertEqual(1, len(numbers))
rps = {row['resource provider'] for row in rows}
self.assertEqual(2, len(rps))
self.assertIn(self.rp1_1['uuid'], rps)
self.assertIn(self.rp1_2['uuid'], rps)

View File

@ -40,7 +40,11 @@ SUPPORTED_VERSIONS = [
'1.22',
'1.23', # unused
'1.24',
'1.25',
'1.26', # unused
'1.27', # unused
'1.28', # Added for provider allocation (un)set (Ussuri)
'1.29',
]

View File

@ -0,0 +1,27 @@
---
features:
- |
The ``openstack allocation candidate list`` command now supports
``--group`` and ``--group-policy`` option. The ``--group`` option accepts
an integer to group granular requests. If specified, following given
options of resources, required/forbidden traits, and aggregates are
associated to that group and will be satisfied by the same resource
provider in the response. ``--group`` can be repeated to get candidates
from multiple resource providers in a same resource provider tree.
If multiple groups are supplied, the separate groups may or may not be
satisfied by the same provider. If you want the groups to be satisfied by
different resource providers, set ``--group_policy`` to ``isolate``.
For example::
openstack allocation candidate list \
--group 1 --resource VCPU=3 --required HW_CPU_X86_SSE \
--group 2 --resource VCPU=4 \
--group_policy isolate
This option is available with ``--os-placement-api-version 1.25`` or
greater, but to have placement server be aware of nested providers, use
``--os-placement-api-version 1.29`` or greater.
See the `REST API Version History`__ for more details.
.. __: https://docs.openstack.org/placement/latest/placement-api-microversion-history.html