diff --git a/openstackclient/tests/volume/v2/fakes.py b/openstackclient/tests/volume/v2/fakes.py index f28a3e25f3..4f5f9cfdcb 100644 --- a/openstackclient/tests/volume/v2/fakes.py +++ b/openstackclient/tests/volume/v2/fakes.py @@ -117,6 +117,47 @@ BACKUP = { BACKUP_columns = tuple(sorted(BACKUP)) BACKUP_data = tuple((BACKUP[x] for x in sorted(BACKUP))) +qos_id = '6f2be1de-997b-4230-b76c-a3633b59e8fb' +qos_consumer = 'front-end' +qos_default_consumer = 'both' +qos_name = "fake-qos-specs" +qos_specs = { + 'foo': 'bar', + 'iops': '9001' +} +qos_association = { + 'association_type': 'volume_type', + 'name': type_name, + 'id': type_id +} + +QOS = { + 'id': qos_id, + 'consumer': qos_consumer, + 'name': qos_name +} + +QOS_DEFAULT_CONSUMER = { + 'id': qos_id, + 'consumer': qos_default_consumer, + 'name': qos_name +} + +QOS_WITH_SPECS = { + 'id': qos_id, + 'consumer': qos_consumer, + 'name': qos_name, + 'specs': qos_specs +} + +QOS_WITH_ASSOCIATIONS = { + 'id': qos_id, + 'consumer': qos_consumer, + 'name': qos_name, + 'specs': qos_specs, + 'associations': [qos_association] +} + class FakeVolumeClient(object): def __init__(self, **kwargs): @@ -130,6 +171,8 @@ class FakeVolumeClient(object): self.volume_types.resource_class = fakes.FakeResource(None, {}) self.restores = mock.Mock() self.restores.resource_class = fakes.FakeResource(None, {}) + self.qos_specs = mock.Mock() + self.qos_specs.resource_class = fakes.FakeResource(None, {}) self.auth_token = kwargs['token'] self.management_url = kwargs['endpoint'] diff --git a/openstackclient/tests/volume/v2/test_qos_specs.py b/openstackclient/tests/volume/v2/test_qos_specs.py new file mode 100644 index 0000000000..92b3f1793f --- /dev/null +++ b/openstackclient/tests/volume/v2/test_qos_specs.py @@ -0,0 +1,446 @@ +# Copyright 2015 iWeb Technologies Inc. +# +# 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 copy + +from openstackclient.common import utils +from openstackclient.tests import fakes +from openstackclient.tests.volume.v2 import fakes as volume_fakes +from openstackclient.volume.v2 import qos_specs + + +class TestQos(volume_fakes.TestVolume): + + def setUp(self): + super(TestQos, self).setUp() + + self.qos_mock = self.app.client_manager.volume.qos_specs + self.qos_mock.reset_mock() + + self.types_mock = self.app.client_manager.volume.volume_types + self.types_mock.reset_mock() + + +class TestQosAssociate(TestQos): + def setUp(self): + super(TestQosAssociate, self).setUp() + + # Get the command object to test + self.cmd = qos_specs.AssociateQos(self.app, None) + + def test_qos_associate(self): + self.qos_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(volume_fakes.QOS), + loaded=True + ) + self.types_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(volume_fakes.TYPE), + loaded=True + ) + arglist = [ + volume_fakes.qos_id, + volume_fakes.type_id + ] + verifylist = [ + ('qos_specs', volume_fakes.qos_id), + ('volume_type', volume_fakes.type_id) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + self.qos_mock.associate.assert_called_with( + volume_fakes.qos_id, + volume_fakes.type_id + ) + + +class TestQosCreate(TestQos): + def setUp(self): + super(TestQosCreate, self).setUp() + + # Get the command object to test + self.cmd = qos_specs.CreateQos(self.app, None) + + def test_qos_create_without_properties(self): + self.qos_mock.create.return_value = fakes.FakeResource( + None, + copy.deepcopy(volume_fakes.QOS_DEFAULT_CONSUMER), + loaded=True + ) + + arglist = [ + volume_fakes.qos_name, + ] + verifylist = [ + ('name', volume_fakes.qos_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.qos_mock.create.assert_called_with( + volume_fakes.qos_name, + {'consumer': volume_fakes.qos_default_consumer} + ) + + collist = ( + 'consumer', + 'id', + 'name' + ) + self.assertEqual(collist, columns) + datalist = ( + volume_fakes.qos_default_consumer, + volume_fakes.qos_id, + volume_fakes.qos_name + ) + self.assertEqual(datalist, data) + + def test_qos_create_with_consumer(self): + self.qos_mock.create.return_value = fakes.FakeResource( + None, + copy.deepcopy(volume_fakes.QOS), + loaded=True + ) + + arglist = [ + volume_fakes.qos_name, + '--consumer', volume_fakes.qos_consumer + ] + verifylist = [ + ('name', volume_fakes.qos_name), + ('consumer', volume_fakes.qos_consumer) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.qos_mock.create.assert_called_with( + volume_fakes.qos_name, + {'consumer': volume_fakes.qos_consumer} + ) + + collist = ( + 'consumer', + 'id', + 'name' + ) + self.assertEqual(collist, columns) + datalist = ( + volume_fakes.qos_consumer, + volume_fakes.qos_id, + volume_fakes.qos_name + ) + self.assertEqual(datalist, data) + + def test_qos_create_with_properties(self): + self.qos_mock.create.return_value = fakes.FakeResource( + None, + copy.deepcopy(volume_fakes.QOS_WITH_SPECS), + loaded=True + ) + + arglist = [ + volume_fakes.qos_name, + '--consumer', volume_fakes.qos_consumer, + '--property', 'foo=bar', + '--property', 'iops=9001' + ] + verifylist = [ + ('name', volume_fakes.qos_name), + ('consumer', volume_fakes.qos_consumer), + ('property', volume_fakes.qos_specs) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + specs = volume_fakes.qos_specs.copy() + specs.update({'consumer': volume_fakes.qos_consumer}) + self.qos_mock.create.assert_called_with( + volume_fakes.qos_name, + specs + ) + + collist = ( + 'consumer', + 'id', + 'name', + 'specs', + ) + self.assertEqual(collist, columns) + datalist = ( + volume_fakes.qos_consumer, + volume_fakes.qos_id, + volume_fakes.qos_name, + volume_fakes.qos_specs, + ) + self.assertEqual(datalist, data) + + +class TestQosDelete(TestQos): + def setUp(self): + super(TestQosDelete, self).setUp() + + self.qos_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(volume_fakes.QOS), + loaded=True, + ) + + # Get the command object to test + self.cmd = qos_specs.DeleteQos(self.app, None) + + def test_qos_delete_with_id(self): + arglist = [ + volume_fakes.qos_id + ] + verifylist = [ + ('qos_specs', volume_fakes.qos_id) + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + self.qos_mock.delete.assert_called_with(volume_fakes.qos_id) + + def test_qos_delete_with_name(self): + arglist = [ + volume_fakes.qos_name + ] + verifylist = [ + ('qos_specs', volume_fakes.qos_name) + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + self.qos_mock.delete.assert_called_with(volume_fakes.qos_id) + + +class TestQosDisassociate(TestQos): + def setUp(self): + super(TestQosDisassociate, self).setUp() + + # Get the command object to test + self.cmd = qos_specs.DisassociateQos(self.app, None) + + def test_qos_disassociate_with_volume_type(self): + self.qos_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(volume_fakes.QOS), + loaded=True + ) + self.types_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(volume_fakes.TYPE), + loaded=True + ) + arglist = [ + volume_fakes.qos_id, + '--volume-type', volume_fakes.type_id + ] + verifylist = [ + ('qos_specs', volume_fakes.qos_id), + ('volume_type', volume_fakes.type_id) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + self.qos_mock.disassociate.assert_called_with( + volume_fakes.qos_id, + volume_fakes.type_id + ) + + def test_qos_disassociate_with_all_volume_types(self): + self.qos_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(volume_fakes.QOS), + loaded=True + ) + + arglist = [ + volume_fakes.qos_id, + '--all' + ] + verifylist = [ + ('qos_specs', volume_fakes.qos_id) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + self.qos_mock.disassociate_all.assert_called_with(volume_fakes.qos_id) + + +class TestQosList(TestQos): + def setUp(self): + super(TestQosList, self).setUp() + + self.qos_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(volume_fakes.QOS_WITH_ASSOCIATIONS), + loaded=True, + ) + self.qos_mock.list.return_value = [self.qos_mock.get.return_value] + self.qos_mock.get_associations.return_value = [fakes.FakeResource( + None, + copy.deepcopy(volume_fakes.qos_association), + loaded=True, + )] + + # Get the command object to test + self.cmd = qos_specs.ListQos(self.app, None) + + def test_qos_list(self): + arglist = [] + verifylist = [] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + self.qos_mock.list.assert_called() + + collist = ( + 'ID', + 'Name', + 'Consumer', + 'Associations', + 'Specs', + ) + self.assertEqual(collist, columns) + datalist = (( + volume_fakes.qos_id, + volume_fakes.qos_name, + volume_fakes.qos_consumer, + volume_fakes.type_name, + utils.format_dict(volume_fakes.qos_specs), + ), ) + self.assertEqual(datalist, tuple(data)) + + +class TestQosSet(TestQos): + def setUp(self): + super(TestQosSet, self).setUp() + + # Get the command object to test + self.cmd = qos_specs.SetQos(self.app, None) + + def test_qos_set_with_properties_with_id(self): + self.qos_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(volume_fakes.QOS_WITH_SPECS), + loaded=True + ) + arglist = [ + volume_fakes.qos_id, + '--property', 'foo=bar', + '--property', 'iops=9001' + ] + verifylist = [ + ('qos_specs', volume_fakes.qos_id), + ('property', volume_fakes.qos_specs) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + self.qos_mock.set_keys.assert_called_with( + volume_fakes.qos_id, + volume_fakes.qos_specs + ) + + +class TestQosShow(TestQos): + def setUp(self): + super(TestQosShow, self).setUp() + + self.qos_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(volume_fakes.QOS_WITH_ASSOCIATIONS), + loaded=True, + ) + self.qos_mock.get_associations.return_value = [fakes.FakeResource( + None, + copy.deepcopy(volume_fakes.qos_association), + loaded=True, + )] + + # Get the command object to test + self.cmd = qos_specs.ShowQos(self.app, None) + + def test_qos_show(self): + arglist = [ + volume_fakes.qos_id + ] + verifylist = [ + ('qos_specs', volume_fakes.qos_id) + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + self.qos_mock.get.assert_called_with( + volume_fakes.qos_id + ) + + collist = ( + 'associations', + 'consumer', + 'id', + 'name', + 'specs' + ) + self.assertEqual(collist, columns) + datalist = ( + volume_fakes.type_name, + volume_fakes.qos_consumer, + volume_fakes.qos_id, + volume_fakes.qos_name, + utils.format_dict(volume_fakes.qos_specs), + ) + self.assertEqual(datalist, tuple(data)) + + +class TestQosUnset(TestQos): + def setUp(self): + super(TestQosUnset, self).setUp() + + # Get the command object to test + self.cmd = qos_specs.UnsetQos(self.app, None) + + def test_qos_unset_with_properties(self): + self.qos_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(volume_fakes.QOS), + loaded=True + ) + arglist = [ + volume_fakes.qos_id, + '--property', 'iops', + '--property', 'foo' + ] + + verifylist = [ + ('qos_specs', volume_fakes.qos_id), + ('property', ['iops', 'foo']) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + self.qos_mock.unset_keys.assert_called_with( + volume_fakes.qos_id, + ['iops', 'foo'] + ) diff --git a/openstackclient/volume/v2/qos_specs.py b/openstackclient/volume/v2/qos_specs.py new file mode 100644 index 0000000000..7f02fa4a2d --- /dev/null +++ b/openstackclient/volume/v2/qos_specs.py @@ -0,0 +1,303 @@ +# Copyright 2015 iWeb Technologies Inc. +# +# 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. +# + +"""Volume v2 QoS action implementations""" + +import logging +import six + +from cliff import command +from cliff import lister +from cliff import show + +from openstackclient.common import parseractions +from openstackclient.common import utils + + +class AssociateQos(command.Command): + """Associate a QoS specification to a volume type""" + + log = logging.getLogger(__name__ + '.AssociateQos') + + def get_parser(self, prog_name): + parser = super(AssociateQos, self).get_parser(prog_name) + parser.add_argument( + 'qos_specs', + metavar='', + help='QoS specification to modify (name or ID)', + ) + parser.add_argument( + 'volume_type', + metavar='', + help='Volume type to associate the QoS (name or ID)', + ) + return parser + + def take_action(self, parsed_args): + self.log.debug('take_action(%s)', parsed_args) + volume_client = self.app.client_manager.volume + qos_specs = utils.find_resource(volume_client.qos_specs, + parsed_args.qos_specs) + volume_type = utils.find_resource(volume_client.volume_types, + parsed_args.volume_type) + + volume_client.qos_specs.associate(qos_specs.id, volume_type.id) + + return + + +class CreateQos(show.ShowOne): + """Create new QoS specification""" + + log = logging.getLogger(__name__ + '.CreateQos') + + def get_parser(self, prog_name): + parser = super(CreateQos, self).get_parser(prog_name) + parser.add_argument( + 'name', + metavar='', + help='New QoS specification name', + ) + consumer_choices = ['front-end', 'back-end', 'both'] + parser.add_argument( + '--consumer', + metavar='', + choices=consumer_choices, + default='both', + help='Consumer of the QoS. Valid consumers: %s ' + "(defaults to 'both')" % utils.format_list(consumer_choices) + ) + parser.add_argument( + '--property', + metavar='', + action=parseractions.KeyValueAction, + help='Set a QoS specification property ' + '(repeat option to set multiple properties)', + ) + return parser + + def take_action(self, parsed_args): + self.log.debug('take_action(%s)', parsed_args) + volume_client = self.app.client_manager.volume + specs = {} + specs.update({'consumer': parsed_args.consumer}) + + if parsed_args.property: + specs.update(parsed_args.property) + + qos_specs = volume_client.qos_specs.create(parsed_args.name, specs) + + return zip(*sorted(six.iteritems(qos_specs._info))) + + +class DeleteQos(command.Command): + """Delete QoS specification""" + + log = logging.getLogger(__name__ + '.DeleteQos') + + def get_parser(self, prog_name): + parser = super(DeleteQos, self).get_parser(prog_name) + parser.add_argument( + 'qos_specs', + metavar='', + help='QoS specification to delete (name or ID)', + ) + return parser + + def take_action(self, parsed_args): + self.log.debug('take_action(%s)', parsed_args) + volume_client = self.app.client_manager.volume + qos_specs = utils.find_resource(volume_client.qos_specs, + parsed_args.qos_specs) + + volume_client.qos_specs.delete(qos_specs.id) + + return + + +class DisassociateQos(command.Command): + """Disassociate a QoS specification from a volume type""" + + log = logging.getLogger(__name__ + '.DisassociateQos') + + def get_parser(self, prog_name): + parser = super(DisassociateQos, self).get_parser(prog_name) + parser.add_argument( + 'qos_specs', + metavar='', + help='QoS specification to modify (name or ID)', + ) + volume_type_group = parser.add_mutually_exclusive_group() + volume_type_group.add_argument( + '--volume-type', + metavar='', + help='Volume type to disassociate the QoS from (name or ID)', + ) + volume_type_group.add_argument( + '--all', + action='store_true', + default=False, + help='Disassociate the QoS from every volume type', + ) + + return parser + + def take_action(self, parsed_args): + self.log.debug('take_action(%s)', parsed_args) + volume_client = self.app.client_manager.volume + qos_specs = utils.find_resource(volume_client.qos_specs, + parsed_args.qos_specs) + + if parsed_args.volume_type: + volume_type = utils.find_resource(volume_client.volume_types, + parsed_args.volume_type) + volume_client.qos_specs.disassociate(qos_specs.id, volume_type.id) + elif parsed_args.all: + volume_client.qos_specs.disassociate_all(qos_specs.id) + + return + + +class ListQos(lister.Lister): + """List QoS specifications""" + + log = logging.getLogger(__name__ + '.ListQos') + + def take_action(self, parsed_args): + self.log.debug('take_action(%s)', parsed_args) + volume_client = self.app.client_manager.volume + qos_specs_list = volume_client.qos_specs.list() + + for qos in qos_specs_list: + qos_associations = volume_client.qos_specs.get_associations(qos) + if qos_associations: + associations = [association.name + for association in qos_associations] + qos._info.update({'associations': associations}) + + columns = ('ID', 'Name', 'Consumer', 'Associations', 'Specs') + return (columns, + (utils.get_dict_properties( + s._info, columns, + formatters={ + 'Specs': utils.format_dict, + 'Associations': utils.format_list + }, + ) for s in qos_specs_list)) + + +class SetQos(command.Command): + """Set QoS specification properties""" + + log = logging.getLogger(__name__ + '.SetQos') + + def get_parser(self, prog_name): + parser = super(SetQos, self).get_parser(prog_name) + parser.add_argument( + 'qos_specs', + metavar='', + help='QoS specification to modify (name or ID)', + ) + parser.add_argument( + '--property', + metavar='', + action=parseractions.KeyValueAction, + help='Property to add or modify for this QoS specification ' + '(repeat option to set multiple properties)', + ) + return parser + + def take_action(self, parsed_args): + self.log.debug('take_action(%s)', parsed_args) + volume_client = self.app.client_manager.volume + qos_specs = utils.find_resource(volume_client.qos_specs, + parsed_args.qos_specs) + + if parsed_args.property: + volume_client.qos_specs.set_keys(qos_specs.id, + parsed_args.property) + else: + self.app.log.error("No changes requested\n") + + return + + +class ShowQos(show.ShowOne): + """Display QoS specification details""" + + log = logging.getLogger(__name__ + '.ShowQos') + + def get_parser(self, prog_name): + parser = super(ShowQos, self).get_parser(prog_name) + parser.add_argument( + 'qos_specs', + metavar='', + help='QoS specification to display (name or ID)', + ) + return parser + + def take_action(self, parsed_args): + self.log.debug('take_action(%s)', parsed_args) + volume_client = self.app.client_manager.volume + qos_specs = utils.find_resource(volume_client.qos_specs, + parsed_args.qos_specs) + + qos_associations = volume_client.qos_specs.get_associations(qos_specs) + if qos_associations: + associations = [association.name + for association in qos_associations] + qos_specs._info.update({ + 'associations': utils.format_list(associations) + }) + qos_specs._info.update({'specs': utils.format_dict(qos_specs.specs)}) + + return zip(*sorted(six.iteritems(qos_specs._info))) + + +class UnsetQos(command.Command): + """Unset QoS specification properties""" + + log = logging.getLogger(__name__ + '.SetQos') + + def get_parser(self, prog_name): + parser = super(UnsetQos, self).get_parser(prog_name) + parser.add_argument( + 'qos_specs', + metavar='', + help='QoS specification to modify (name or ID)', + ) + parser.add_argument( + '--property', + metavar='', + action='append', + default=[], + help='Property to remove from the QoS specification. ' + '(repeat option to unset multiple properties)', + ) + return parser + + def take_action(self, parsed_args): + self.log.debug('take_action(%s)', parsed_args) + volume_client = self.app.client_manager.volume + qos_specs = utils.find_resource(volume_client.qos_specs, + parsed_args.qos_specs) + + if parsed_args.property: + volume_client.qos_specs.unset_keys(qos_specs.id, + parsed_args.property) + else: + self.app.log.error("No changes requested\n") + + return diff --git a/setup.cfg b/setup.cfg index 7fb227e67b..b0343e0987 100644 --- a/setup.cfg +++ b/setup.cfg @@ -395,6 +395,15 @@ openstack.volume.v2 = volume_type_delete = openstackclient.volume.v2.volume_type:DeleteVolumeType volume_type_show = openstackclient.volume.v2.volume_type:ShowVolumeType + volume_qos_associate = openstackclient.volume.v2.qos_specs:AssociateQos + volume_qos_create = openstackclient.volume.v2.qos_specs:CreateQos + volume_qos_delete = openstackclient.volume.v2.qos_specs:DeleteQos + volume_qos_disassociate = openstackclient.volume.v2.qos_specs:DisassociateQos + volume_qos_list = openstackclient.volume.v2.qos_specs:ListQos + volume_qos_set = openstackclient.volume.v2.qos_specs:SetQos + volume_qos_show = openstackclient.volume.v2.qos_specs:ShowQos + volume_qos_unset = openstackclient.volume.v2.qos_specs:UnsetQos + [build_sphinx] source-dir = doc/source build-dir = doc/build