CLI for aggregates (v1.1)
Change-Id: Ia37c051c7451d174b8dbed8ab1bc13b6f2f0a3fe
This commit is contained in:
parent
4fd95eb2a8
commit
e22c32e441
@ -21,7 +21,12 @@ LOG = logging.getLogger(__name__)
|
||||
|
||||
API_NAME = 'placement'
|
||||
API_VERSION_OPTION = 'os_placement_api_version'
|
||||
API_VERSIONS = {'1.0': 'osc_placement.http.SessionClient'}
|
||||
SUPPORTED_VERSIONS = [
|
||||
'1.0',
|
||||
'1.1'
|
||||
]
|
||||
API_VERSIONS = {v: 'osc_placement.http.SessionClient'
|
||||
for v in SUPPORTED_VERSIONS}
|
||||
|
||||
|
||||
def make_client(instance):
|
||||
|
86
osc_placement/resources/aggregate.py
Normal file
86
osc_placement/resources/aggregate.py
Normal file
@ -0,0 +1,86 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from osc_lib.command import command
|
||||
from osc_placement import version
|
||||
|
||||
|
||||
BASE_URL = '/resource_providers/{uuid}/aggregates'
|
||||
FIELDS = ('uuid',)
|
||||
|
||||
|
||||
class SetAggregate(command.Lister):
|
||||
|
||||
"""Associate a list of aggregates with the resource provider.
|
||||
|
||||
Each request cleans up previously associated resource provider
|
||||
aggregates entirely and sets the new ones. Passing empty aggregate
|
||||
UUID list will remove all associations with aggregates for the
|
||||
particular resource provider.
|
||||
|
||||
This command requires at least --os-placement-api-version 1.1.
|
||||
"""
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(SetAggregate, self).get_parser(prog_name)
|
||||
|
||||
parser.add_argument(
|
||||
'uuid',
|
||||
metavar='<name>',
|
||||
help='UUID of the resource provider'
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--aggregate',
|
||||
metavar='<aggregate_uuid>',
|
||||
help='UUID of the aggregate. Specify multiple times to associate '
|
||||
'a resource provider with multiple aggregates.',
|
||||
action='append',
|
||||
default=[]
|
||||
)
|
||||
|
||||
return parser
|
||||
|
||||
@version.check(version.ge('1.1'))
|
||||
def take_action(self, parsed_args):
|
||||
http = self.app.client_manager.placement
|
||||
|
||||
url = BASE_URL.format(uuid=parsed_args.uuid)
|
||||
resp = http.request('PUT', url, json=parsed_args.aggregate).json()
|
||||
return FIELDS, [[r] for r in resp['aggregates']]
|
||||
|
||||
|
||||
class ListAggregate(command.Lister):
|
||||
|
||||
"""List resource provider aggregates.
|
||||
|
||||
This command requires at least --os-placement-api-version 1.1.
|
||||
"""
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(ListAggregate, self).get_parser(prog_name)
|
||||
|
||||
parser.add_argument(
|
||||
'uuid',
|
||||
metavar='<uuid>',
|
||||
help='UUID of the resource provider'
|
||||
)
|
||||
|
||||
return parser
|
||||
|
||||
@version.check(version.ge('1.1'))
|
||||
def take_action(self, parsed_args):
|
||||
http = self.app.client_manager.placement
|
||||
|
||||
url = BASE_URL.format(uuid=parsed_args.uuid)
|
||||
resp = http.request('GET', url).json()
|
||||
return FIELDS, [[r] for r in resp['aggregates']]
|
@ -22,16 +22,22 @@ RP_PREFIX = 'osc-placement-functional-tests-'
|
||||
|
||||
|
||||
class BaseTestCase(base.BaseTestCase):
|
||||
@staticmethod
|
||||
def openstack(cmd, may_fail=False, use_json=False):
|
||||
VERSION = None
|
||||
|
||||
@classmethod
|
||||
def openstack(cls, cmd, may_fail=False, use_json=False):
|
||||
try:
|
||||
to_exec = ['openstack'] + cmd.split()
|
||||
if use_json:
|
||||
to_exec += ['-f', 'json']
|
||||
if cls.VERSION is not None:
|
||||
to_exec += ['--os-placement-api-version', cls.VERSION]
|
||||
|
||||
output = subprocess.check_output(to_exec, stderr=subprocess.STDOUT)
|
||||
result = (output or b'').decode('utf-8')
|
||||
except subprocess.CalledProcessError:
|
||||
except subprocess.CalledProcessError as e:
|
||||
msg = 'Command: "%s"\noutput: %s' % (' '.join(e.cmd), e.output)
|
||||
e.cmd = msg
|
||||
if not may_fail:
|
||||
raise
|
||||
|
||||
@ -40,6 +46,18 @@ class BaseTestCase(base.BaseTestCase):
|
||||
else:
|
||||
return result
|
||||
|
||||
def assertCommandFailed(self, message, func, *args, **kwargs):
|
||||
signature = [func]
|
||||
signature.extend(args)
|
||||
try:
|
||||
func(*args, **kwargs)
|
||||
self.fail('Command does not fail as required (%s)' % signature)
|
||||
|
||||
except subprocess.CalledProcessError as e:
|
||||
self.assertIn(
|
||||
message, e.output,
|
||||
'Command "%s" fails with different message' % e.cmd)
|
||||
|
||||
def resource_provider_create(self, name=''):
|
||||
if not name:
|
||||
random_part = ''.join(random.choice(string.ascii_letters)
|
||||
@ -143,3 +161,13 @@ class BaseTestCase(base.BaseTestCase):
|
||||
def resource_provider_show_usage(self, uuid):
|
||||
return self.openstack('resource provider usage show ' + uuid,
|
||||
use_json=True)
|
||||
|
||||
def resource_provider_aggregate_list(self, uuid):
|
||||
return self.openstack('resource provider aggregate list ' + uuid,
|
||||
use_json=True)
|
||||
|
||||
def resource_provider_aggregate_set(self, uuid, *aggregates):
|
||||
cmd = 'resource provider aggregate set %s ' % uuid
|
||||
cmd += ' '.join('--aggregate %s' % aggregate
|
||||
for aggregate in aggregates)
|
||||
return self.openstack(cmd, use_json=True)
|
||||
|
88
osc_placement/tests/functional/test_aggregate.py
Normal file
88
osc_placement/tests/functional/test_aggregate.py
Normal file
@ -0,0 +1,88 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import uuid
|
||||
|
||||
from osc_placement.tests.functional import base
|
||||
|
||||
|
||||
class TestAggregate(base.BaseTestCase):
|
||||
VERSION = '1.1'
|
||||
|
||||
def test_fail_if_no_rp(self):
|
||||
self.assertCommandFailed(
|
||||
'too few arguments',
|
||||
self.openstack,
|
||||
'resource provider aggregate list')
|
||||
|
||||
def test_fail_if_rp_not_found(self):
|
||||
self.assertCommandFailed(
|
||||
'No resource provider',
|
||||
self.resource_provider_aggregate_list,
|
||||
'fake-uuid')
|
||||
|
||||
def test_return_empty_list_if_no_aggregates(self):
|
||||
rp = self.resource_provider_create()
|
||||
self.assertEqual(
|
||||
[], self.resource_provider_aggregate_list(rp['uuid']))
|
||||
|
||||
def test_success_set_aggregate(self):
|
||||
rp = self.resource_provider_create()
|
||||
aggs = {str(uuid.uuid4()) for _ in range(2)}
|
||||
rows = self.resource_provider_aggregate_set(
|
||||
rp['uuid'], *aggs)
|
||||
|
||||
self.assertEqual(aggs, {r['uuid'] for r in rows})
|
||||
rows = self.resource_provider_aggregate_list(rp['uuid'])
|
||||
self.assertEqual(aggs, {r['uuid'] for r in rows})
|
||||
self.resource_provider_aggregate_set(rp['uuid'])
|
||||
rows = self.resource_provider_aggregate_list(rp['uuid'])
|
||||
self.assertEqual([], rows)
|
||||
|
||||
def test_set_aggregate_fail_if_no_rp(self):
|
||||
self.assertCommandFailed(
|
||||
'too few arguments',
|
||||
self.openstack,
|
||||
'resource provider aggregate set')
|
||||
|
||||
def test_success_set_multiple_aggregates(self):
|
||||
# each rp is associated with two aggregates
|
||||
rps = [self.resource_provider_create() for _ in range(2)]
|
||||
aggs = {str(uuid.uuid4()) for _ in range(2)}
|
||||
for rp in rps:
|
||||
rows = self.resource_provider_aggregate_set(rp['uuid'], *aggs)
|
||||
self.assertEqual(aggs, {r['uuid'] for r in rows})
|
||||
# remove association for the first aggregate
|
||||
rows = self.resource_provider_aggregate_set(rps[0]['uuid'])
|
||||
self.assertEqual([], rows)
|
||||
# second rp should be in aggregates
|
||||
rows = self.resource_provider_aggregate_list(rps[1]['uuid'])
|
||||
self.assertEqual(aggs, {r['uuid'] for r in rows})
|
||||
# cleanup
|
||||
rows = self.resource_provider_aggregate_set(rps[1]['uuid'])
|
||||
self.assertEqual([], rows)
|
||||
|
||||
def test_success_set_large_number_aggregates(self):
|
||||
rp = self.resource_provider_create()
|
||||
aggs = {str(uuid.uuid4()) for _ in range(100)}
|
||||
rows = self.resource_provider_aggregate_set(
|
||||
rp['uuid'], *aggs)
|
||||
self.assertEqual(aggs, {r['uuid'] for r in rows})
|
||||
rows = self.resource_provider_aggregate_set(rp['uuid'])
|
||||
self.assertEqual([], rows)
|
||||
|
||||
def test_fail_if_incorrect_aggregate_uuid(self):
|
||||
rp = self.resource_provider_create()
|
||||
self.assertCommandFailed(
|
||||
"is not a 'uuid'",
|
||||
self.resource_provider_aggregate_set,
|
||||
rp['uuid'], 'abc', 'efg')
|
82
osc_placement/tests/unit/test_version.py
Normal file
82
osc_placement/tests/unit/test_version.py
Normal file
@ -0,0 +1,82 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import mock
|
||||
|
||||
import oslotest.base as base
|
||||
|
||||
from osc_placement import version
|
||||
|
||||
|
||||
class TestVersion(base.BaseTestCase):
|
||||
def test_compare(self):
|
||||
self.assertTrue(version._compare('1.0', version.gt('0.9')))
|
||||
self.assertTrue(version._compare('1.0', version.ge('0.9')))
|
||||
self.assertTrue(version._compare('1.0', version.ge('1.0')))
|
||||
self.assertTrue(version._compare('1.0', version.eq('1.0')))
|
||||
self.assertTrue(version._compare('1.0', version.le('1.0')))
|
||||
self.assertTrue(version._compare('1.0', version.le('1.1')))
|
||||
self.assertTrue(version._compare('1.0', version.lt('1.1')))
|
||||
self.assertTrue(
|
||||
version._compare('1.1', version.gt('1.0'), version.lt('1.2')))
|
||||
self.assertTrue(
|
||||
version._compare(
|
||||
'0.3', version.eq('0.2'), version.eq('0.3'), op=any))
|
||||
self.assertFalse(version._compare('1.0', version.gt('1.0')))
|
||||
self.assertFalse(version._compare('1.0', version.ge('1.1')))
|
||||
self.assertFalse(version._compare('1.0', version.eq('1.1')))
|
||||
self.assertFalse(version._compare('1.0', version.le('0.9')))
|
||||
self.assertFalse(version._compare('1.0', version.lt('0.9')))
|
||||
self.assertRaises(
|
||||
ValueError, version._compare, 'abc', version.le('1.1'))
|
||||
self.assertRaises(
|
||||
ValueError, version._compare, '1.0', version.le('.0'))
|
||||
self.assertRaises(
|
||||
ValueError, version._compare, '1', version.le('2'))
|
||||
|
||||
def test_compare_with_exc(self):
|
||||
self.assertTrue(version.compare('1.05', version.gt('1.4')))
|
||||
self.assertFalse(version.compare('1.3', version.gt('1.4'), exc=False))
|
||||
self.assertRaisesRegex(
|
||||
ValueError,
|
||||
'Operation or argument is not supported',
|
||||
version.compare, '3.1.2', version.gt('3.1.3'))
|
||||
|
||||
def test_check_decorator(self):
|
||||
fake_api = mock.Mock()
|
||||
fake_api_dec = version.check(version.gt('2.11'))(fake_api)
|
||||
obj = mock.Mock()
|
||||
obj.app.client_manager.placement.api_version = '2.12'
|
||||
fake_api_dec(obj, 1, 2, 3)
|
||||
fake_api.assert_called_once_with(obj, 1, 2, 3)
|
||||
fake_api.reset_mock()
|
||||
obj.app.client_manager.placement.api_version = '2.10'
|
||||
self.assertRaisesRegex(
|
||||
ValueError,
|
||||
'Operation or argument is not supported',
|
||||
fake_api_dec,
|
||||
obj, 1, 2, 3)
|
||||
fake_api.assert_not_called()
|
||||
|
||||
def test_check_mixin(self):
|
||||
|
||||
class Test(version.CheckerMixin):
|
||||
app = mock.Mock()
|
||||
app.client_manager.placement.api_version = '1.2'
|
||||
|
||||
t = Test()
|
||||
self.assertTrue(t.compare_version(version.le('1.3')))
|
||||
self.assertTrue(t.check_version(version.ge('1.0')))
|
||||
self.assertRaisesRegex(
|
||||
ValueError,
|
||||
'Operation or argument is not supported',
|
||||
t.check_version, version.lt('1.2'))
|
77
osc_placement/version.py
Normal file
77
osc_placement/version.py
Normal file
@ -0,0 +1,77 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from distutils.version import StrictVersion
|
||||
import operator
|
||||
|
||||
|
||||
def _op(func, b):
|
||||
return lambda a: func(StrictVersion(a), StrictVersion(b))
|
||||
|
||||
|
||||
def lt(b):
|
||||
return _op(operator.lt, b)
|
||||
|
||||
|
||||
def le(b):
|
||||
return _op(operator.le, b)
|
||||
|
||||
|
||||
def eq(b):
|
||||
return _op(operator.eq, b)
|
||||
|
||||
|
||||
def ne(b):
|
||||
return _op(operator.ne, b)
|
||||
|
||||
|
||||
def ge(b):
|
||||
return _op(operator.ge, b)
|
||||
|
||||
|
||||
def gt(b):
|
||||
return _op(operator.gt, b)
|
||||
|
||||
|
||||
def _compare(ver, *predicates, **kwargs):
|
||||
func = kwargs.get('op', all)
|
||||
return func(p(ver) for p in predicates)
|
||||
|
||||
|
||||
def compare(ver, *predicates, **kwargs):
|
||||
exc = kwargs.get('exc', True)
|
||||
if not _compare(ver, *predicates, **kwargs):
|
||||
if exc:
|
||||
raise ValueError(
|
||||
'Operation or argument is not supported with version %s' % ver)
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def check(*predicates, **check_kwargs):
|
||||
def wrapped(func):
|
||||
def inner(self, *args, **kwargs):
|
||||
version = self.app.client_manager.placement.api_version
|
||||
compare(version, *predicates, **check_kwargs)
|
||||
return func(self, *args, **kwargs)
|
||||
return inner
|
||||
return wrapped
|
||||
|
||||
|
||||
class CheckerMixin(object):
|
||||
def check_version(self, *predicates, **kwargs):
|
||||
version = self.app.client_manager.placement.api_version
|
||||
return compare(version, *predicates, **kwargs)
|
||||
|
||||
def compare_version(self, *predicates, **kwargs):
|
||||
version = self.app.client_manager.placement.api_version
|
||||
return compare(version, *predicates, exc=False, **kwargs)
|
@ -35,13 +35,14 @@ openstack.placement.v1 =
|
||||
resource_provider_show = osc_placement.resources.resource_provider:ShowResourceProvider
|
||||
resource_provider_set = osc_placement.resources.resource_provider:SetResourceProvider
|
||||
resource_provider_delete = osc_placement.resources.resource_provider:DeleteResourceProvider
|
||||
resource provider_usage_show = osc_placement.resources.usage:ShowUsage
|
||||
resource_provider_inventory_set = osc_placement.resources.inventory:SetInventory
|
||||
resource_provider_inventory_class_set = osc_placement.resources.inventory:SetClassInventory
|
||||
resource_provider_inventory_list = osc_placement.resources.inventory:ListInventory
|
||||
resource_provider_inventory_show = osc_placement.resources.inventory:ShowInventory
|
||||
resource_provider_inventory_delete = osc_placement.resources.inventory:DeleteInventory
|
||||
resource provider_usage_show = osc_placement.resources.usage:ShowUsage
|
||||
|
||||
resource_provider_aggregate_list = osc_placement.resources.aggregate:ListAggregate
|
||||
resource_provider_aggregate_set = osc_placement.resources.aggregate:SetAggregate
|
||||
|
||||
[build_sphinx]
|
||||
source-dir = doc/source
|
||||
|
Loading…
Reference in New Issue
Block a user