CLI for aggregates (v1.1)

Change-Id: Ia37c051c7451d174b8dbed8ab1bc13b6f2f0a3fe
This commit is contained in:
Andrey Volkov 2017-09-20 13:54:59 +03:00 committed by Matt Riedemann
parent 4fd95eb2a8
commit e22c32e441
7 changed files with 373 additions and 6 deletions

View File

@ -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):

View 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']]

View File

@ -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)

View 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')

View 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
View 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)

View File

@ -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