Merge "Support microversion 1.39"
This commit is contained in:
commit
aa14e9ccde
@ -16,6 +16,7 @@ import collections
|
||||
from osc_lib.command import command
|
||||
from osc_lib import exceptions
|
||||
|
||||
from osc_placement.resources import common
|
||||
from osc_placement import version
|
||||
|
||||
|
||||
@ -102,7 +103,10 @@ class ListAllocationCandidate(command.Lister, version.CheckerMixin):
|
||||
help='A required trait. May be repeated. Allocation candidates '
|
||||
'must collectively contain all of the required traits. '
|
||||
'This option requires at least '
|
||||
'``--os-placement-api-version 1.17``.'
|
||||
'``--os-placement-api-version 1.17``. '
|
||||
'Since ``--os-placement-api-version 1.39`` the value of '
|
||||
'this parameter can be a comma separated list of trait names '
|
||||
'to express OR relationship between those traits.'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--forbidden',
|
||||
@ -204,18 +208,31 @@ class ListAllocationCandidate(command.Lister, version.CheckerMixin):
|
||||
|
||||
params[_get_key('resources')] = ','.join(
|
||||
resource.replace('=', ':') for resource in group['resources'])
|
||||
|
||||
# We need to handle required and forbidden together as they all
|
||||
# end up in the same query param on the API.
|
||||
# First just check that the requested feature is aligned with the
|
||||
# request microversion
|
||||
required_traits = []
|
||||
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 any(',' in required for required in group['required']):
|
||||
self.check_version(version.ge('1.39'))
|
||||
required_traits = group['required']
|
||||
|
||||
forbidden_traits = []
|
||||
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
|
||||
forbidden_traits = ['!' + f for f in group['forbidden']]
|
||||
|
||||
# Then collect the required query params containing both required
|
||||
# and forbidden traits
|
||||
params[_get_key('required')] = (
|
||||
common.get_required_query_param_from_args(
|
||||
required_traits, 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'))
|
||||
|
@ -36,3 +36,23 @@ def url_with_filters(url, filters=None):
|
||||
url = urlparse.urljoin(url, '?' + urlencoded_filters)
|
||||
|
||||
return url
|
||||
|
||||
|
||||
def get_required_query_param_from_args(required_traits, forbidden_traits):
|
||||
# Iterate the required params and collect OR groups and simple
|
||||
# AND traits separately. Each OR group needs a separate query param
|
||||
# while the AND traits and forbidden traits can be collated to a single
|
||||
# query param
|
||||
required_query_params = []
|
||||
and_traits = []
|
||||
for required in required_traits:
|
||||
if ',' in required:
|
||||
required_query_params.append('in:' + required)
|
||||
else:
|
||||
and_traits.append(required)
|
||||
# We need an extra required query param for the and_traits and the
|
||||
# forbidden traits
|
||||
and_query = ','.join(and_traits + forbidden_traits)
|
||||
if and_query:
|
||||
required_query_params.append(and_query)
|
||||
return required_query_params
|
||||
|
@ -15,6 +15,7 @@ import argparse
|
||||
from osc_lib.command import command
|
||||
from osc_lib import utils
|
||||
|
||||
from osc_placement.resources import common
|
||||
from osc_placement import version
|
||||
|
||||
|
||||
@ -114,7 +115,10 @@ class ListResourceProvider(command.Lister, version.CheckerMixin):
|
||||
help='A required trait. May be repeated. Resource providers '
|
||||
'must collectively contain all of the required traits. '
|
||||
'This option requires at least '
|
||||
'``--os-placement-api-version 1.18``.'
|
||||
'``--os-placement-api-version 1.18``. '
|
||||
'Since ``--os-placement-api-version 1.39`` the value of '
|
||||
'this parameter can be a comma separated list of trait names '
|
||||
'to express OR relationship between those traits.'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--forbidden',
|
||||
@ -175,17 +179,28 @@ class ListResourceProvider(command.Lister, version.CheckerMixin):
|
||||
if 'in_tree' in parsed_args and parsed_args.in_tree:
|
||||
self.check_version(version.ge('1.14'))
|
||||
filters['in_tree'] = parsed_args.in_tree
|
||||
|
||||
# We need to handle required and forbidden together as they all end up
|
||||
# in the same query param on the API.
|
||||
# First just check that the requested feature is aligned with the
|
||||
# request microversion
|
||||
required_traits = []
|
||||
if 'required' in parsed_args and parsed_args.required:
|
||||
self.check_version(version.ge('1.18'))
|
||||
filters['required'] = ','.join(parsed_args.required)
|
||||
if any(',' in required for required in parsed_args.required):
|
||||
self.check_version(version.ge('1.39'))
|
||||
required_traits = parsed_args.required
|
||||
|
||||
forbidden_traits = []
|
||||
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 filters:
|
||||
filters['required'] += ',' + forbidden_traits
|
||||
else:
|
||||
filters['required'] = forbidden_traits
|
||||
forbidden_traits = ['!' + f for f in parsed_args.forbidden]
|
||||
|
||||
# Then collect the required query params containing both required and
|
||||
# forbidden traits
|
||||
filters['required'] = common.get_required_query_param_from_args(
|
||||
required_traits, forbidden_traits)
|
||||
|
||||
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.3'))
|
||||
|
@ -444,7 +444,8 @@ class BaseTestCase(base.BaseTestCase):
|
||||
limit=None):
|
||||
cmd = 'allocation candidate list '
|
||||
for suffix, req_group in groups.items():
|
||||
cmd += ' --group %s' % suffix
|
||||
if suffix:
|
||||
cmd += ' --group %s' % suffix
|
||||
cmd += self._allocation_candidates_option(**req_group)
|
||||
if limit is not None:
|
||||
cmd += ' --limit %d' % limit
|
||||
|
@ -420,3 +420,141 @@ class TestAllocationCandidate129(base.BaseTestCase):
|
||||
self.assertEqual(2, len(rps))
|
||||
self.assertIn(self.rp1_1['uuid'], rps)
|
||||
self.assertIn(self.rp1_2['uuid'], rps)
|
||||
|
||||
def test_list_with_any_traits_old_microversion(self):
|
||||
groups = {
|
||||
'': {
|
||||
'resources': ('DISK_GB=1',),
|
||||
'required': ('STORAGE_DISK_HDD,STORAGE_DISK_SSD',),
|
||||
},
|
||||
'1': {
|
||||
'resources': ('VCPU=1',),
|
||||
'required': ('HW_CPU_X86_AVX',),
|
||||
}
|
||||
}
|
||||
self.assertCommandFailed(
|
||||
'Operation or argument is not supported with version 1.29',
|
||||
self.allocation_candidate_granular, groups=groups
|
||||
)
|
||||
|
||||
|
||||
class TestAllocationCandidate139(base.BaseTestCase):
|
||||
VERSION = '1.39'
|
||||
|
||||
def setUp(self):
|
||||
super(TestAllocationCandidate139, 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.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['uuid'], 'STORAGE_DISK_HDD')
|
||||
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')
|
||||
|
||||
self.rp2 = self.resource_provider_create()
|
||||
self.rp2_1 = self.resource_provider_create(
|
||||
parent_provider_uuid=self.rp2['uuid'])
|
||||
self.rp2_2 = self.resource_provider_create(
|
||||
parent_provider_uuid=self.rp2['uuid'])
|
||||
|
||||
self.resource_inventory_set(self.rp2['uuid'], 'DISK_GB=512')
|
||||
self.resource_inventory_set(
|
||||
self.rp2_1['uuid'], 'VCPU=8', 'MEMORY_MB=8192')
|
||||
self.resource_inventory_set(
|
||||
self.rp2_2['uuid'], 'VCPU=16', 'MEMORY_MB=8192')
|
||||
|
||||
self.resource_provider_trait_set(self.rp2['uuid'], 'STORAGE_DISK_SSD')
|
||||
self.resource_provider_trait_set(self.rp2_1['uuid'], 'HW_CPU_X86_AVX')
|
||||
self.resource_provider_trait_set(self.rp2_2['uuid'], 'HW_CPU_X86_SSE')
|
||||
|
||||
def test_list_with_any_traits(self):
|
||||
# asking for HDD and AVX that is only on the first tree as the second
|
||||
# has SSD instead
|
||||
groups = {
|
||||
'': {
|
||||
'resources': ('DISK_GB=1',),
|
||||
'required': ('STORAGE_DISK_HDD',),
|
||||
},
|
||||
'1': {
|
||||
'resources': ('VCPU=1',),
|
||||
'required': ('HW_CPU_X86_AVX',),
|
||||
}
|
||||
}
|
||||
rows = self.allocation_candidate_granular(groups=groups)
|
||||
|
||||
# we expect one candidate
|
||||
numbers = {row['#'] for row in rows}
|
||||
self.assertEqual(1, len(numbers))
|
||||
# with two groups satisfied
|
||||
self.assertEqual(2, len(rows))
|
||||
|
||||
rps = {row['resource provider'] for row in rows}
|
||||
self.assertEqual({self.rp1['uuid'], self.rp1_1['uuid']}, rps)
|
||||
|
||||
# extend this by asking for SSD or HDD
|
||||
groups = {
|
||||
'': {
|
||||
'resources': ('DISK_GB=1',),
|
||||
'required': ('STORAGE_DISK_HDD,STORAGE_DISK_SSD',),
|
||||
},
|
||||
'1': {
|
||||
'resources': ('VCPU=1',),
|
||||
'required': ('HW_CPU_X86_AVX',),
|
||||
}
|
||||
}
|
||||
rows = self.allocation_candidate_granular(groups=groups)
|
||||
|
||||
# we expect two candidates now as both tree matches
|
||||
numbers = {row['#'] for row in rows}
|
||||
self.assertEqual(2, len(numbers))
|
||||
# with two groups satisfied each
|
||||
self.assertEqual(4, len(rows))
|
||||
|
||||
rps = {row['resource provider'] for row in rows}
|
||||
self.assertEqual(
|
||||
{
|
||||
self.rp1['uuid'], self.rp1_1['uuid'],
|
||||
self.rp2['uuid'], self.rp2_1['uuid'],
|
||||
},
|
||||
rps
|
||||
)
|
||||
|
||||
# make it crazy by asking for (HDD or SSD) and SSD and not HDD
|
||||
# this basically means SSD but tests all the branches of the client
|
||||
# code
|
||||
# similarly for the granular group ask for (AVX or SSE) and not SSE
|
||||
groups = {
|
||||
'': {
|
||||
'resources': ('DISK_GB=1',),
|
||||
'required': (
|
||||
'STORAGE_DISK_HDD,STORAGE_DISK_SSD',
|
||||
'STORAGE_DISK_SSD'
|
||||
),
|
||||
'forbidden': ('STORAGE_DISK_HDD',),
|
||||
},
|
||||
'1': {
|
||||
'resources': ('VCPU=1',),
|
||||
'required': ('HW_CPU_X86_AVX,HW_CPU_X86_SSE',),
|
||||
'forbidden': ('HW_CPU_X86_SSE',),
|
||||
}
|
||||
}
|
||||
rows = self.allocation_candidate_granular(groups=groups)
|
||||
|
||||
# SSD and AVX means we only the second tree matches with a single
|
||||
# candidate
|
||||
numbers = {row['#'] for row in rows}
|
||||
self.assertEqual(1, len(numbers))
|
||||
# with two groups satisfied
|
||||
self.assertEqual(2, len(rows))
|
||||
|
||||
rps = {row['resource provider'] for row in rows}
|
||||
self.assertEqual({self.rp2['uuid'], self.rp2_1['uuid']}, rps)
|
||||
|
@ -356,7 +356,7 @@ class TestResourceProvider122(base.BaseTestCase):
|
||||
rps = self.resource_provider_list(
|
||||
resources=('MEMORY_MB=1024', 'DISK_GB=80'),
|
||||
required=('HW_CPU_X86_VMX',),
|
||||
forbidden=('!STORAGE_DISK_SSD',))
|
||||
forbidden=('STORAGE_DISK_SSD',))
|
||||
|
||||
uuids = [rp['uuid'] for rp in rps]
|
||||
|
||||
@ -364,3 +364,67 @@ class TestResourceProvider122(base.BaseTestCase):
|
||||
self.assertNotIn(rp1['uuid'], uuids)
|
||||
self.assertIn(rp2['uuid'], uuids)
|
||||
self.assertIn(rp3['uuid'], uuids)
|
||||
|
||||
def test_list_required_trait_any_trait_old_microversion(self):
|
||||
self.assertCommandFailed(
|
||||
'Operation or argument is not supported with version 1.22',
|
||||
self.resource_provider_list,
|
||||
resources=('MEMORY_MB=1024', 'DISK_GB=80'),
|
||||
required=(
|
||||
'STORAGE_DISK_HDD,STORAGE_DISK_SSD',
|
||||
'HW_NIC_SRIOV_MULTIQUEUE'),
|
||||
)
|
||||
|
||||
|
||||
class TestResourceProvider139(base.BaseTestCase):
|
||||
VERSION = '1.39'
|
||||
|
||||
def test_list_required_trait_any_trait(self):
|
||||
rp1 = self.resource_provider_create()
|
||||
rp2 = self.resource_provider_create()
|
||||
self.resource_inventory_set(
|
||||
rp1['uuid'], 'MEMORY_MB=8192', 'DISK_GB=512')
|
||||
self.resource_inventory_set(
|
||||
rp2['uuid'], 'MEMORY_MB=8192', 'DISK_GB=512')
|
||||
self.resource_provider_trait_set(
|
||||
rp1['uuid'], 'STORAGE_DISK_SSD', 'HW_NIC_SRIOV_MULTIQUEUE')
|
||||
self.resource_provider_trait_set(
|
||||
rp2['uuid'], 'STORAGE_DISK_HDD', 'HW_NIC_SRIOV_MULTIQUEUE')
|
||||
|
||||
rps = self.resource_provider_list(
|
||||
resources=('MEMORY_MB=1024', 'DISK_GB=80'),
|
||||
required=('HW_NIC_SRIOV_MULTIQUEUE',))
|
||||
|
||||
self.assertEqual(
|
||||
{rp1['uuid'], rp2['uuid']}, {rp['uuid'] for rp in rps})
|
||||
|
||||
# Narrow the results and check multiple args.
|
||||
rps = self.resource_provider_list(
|
||||
resources=('MEMORY_MB=1024', 'DISK_GB=80'),
|
||||
required=('STORAGE_DISK_HDD', 'HW_NIC_SRIOV_MULTIQUEUE',))
|
||||
|
||||
self.assertEqual({rp2['uuid']}, {rp['uuid'] for rp in rps})
|
||||
|
||||
# Query for (HDD or SSD) and MULTIQUEUE and see that both RP returned
|
||||
# again
|
||||
rps = self.resource_provider_list(
|
||||
resources=('MEMORY_MB=1024', 'DISK_GB=80'),
|
||||
required=(
|
||||
'STORAGE_DISK_HDD,STORAGE_DISK_SSD',
|
||||
'HW_NIC_SRIOV_MULTIQUEUE')
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
{rp1['uuid'], rp2['uuid']}, {rp['uuid'] for rp in rps})
|
||||
|
||||
# Query for (HDD or SSD) and MULTIQUEUE and !SSD and see that one of
|
||||
# the RPs are filtered
|
||||
rps = self.resource_provider_list(
|
||||
resources=('MEMORY_MB=1024', 'DISK_GB=80'),
|
||||
required=(
|
||||
'STORAGE_DISK_HDD,STORAGE_DISK_SSD',
|
||||
'HW_NIC_SRIOV_MULTIQUEUE'),
|
||||
forbidden=('STORAGE_DISK_SSD',),
|
||||
)
|
||||
|
||||
self.assertEqual({rp2['uuid']}, {rp['uuid'] for rp in rps})
|
||||
|
@ -50,6 +50,7 @@ SUPPORTED_MICROVERSIONS = [
|
||||
'1.29',
|
||||
'1.37', # unused
|
||||
'1.38', # Added for consumer types (Xena)
|
||||
'1.39', # Added any-traits support (Yoga)
|
||||
]
|
||||
SUPPORTED_VERSIONS = SUPPORTED_MICROVERSIONS + NEGOTIATE_VERSIONS
|
||||
# The max microversion lower than which are all supported by this client.
|
||||
|
@ -0,0 +1,10 @@
|
||||
---
|
||||
features:
|
||||
- |
|
||||
Both the ``openstack resource provider list`` and
|
||||
``openstack allocation candidate list`` command now supports
|
||||
``--os-placement-api-version 1.39`` where the ``--required`` argument now
|
||||
can contain a comma separated list of trait names to express OR
|
||||
relationship. So ``--required T1,T2 --required T3`` means
|
||||
(T1 or T2) and T3.
|
||||
|
Loading…
x
Reference in New Issue
Block a user