Merge "Support granular allocation candidate list"
This commit is contained in:
commit
d6ebc8c543
|
@ -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()
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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',
|
||||
]
|
||||
|
||||
|
||||
|
|
|
@ -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
|
Loading…
Reference in New Issue