diff --git a/cinderclient/tests/v1/fakes.py b/cinderclient/tests/v1/fakes.py index 2c052c6..87c9b39 100644 --- a/cinderclient/tests/v1/fakes.py +++ b/cinderclient/tests/v1/fakes.py @@ -116,6 +116,34 @@ def _stub_restore(): return {'volume_id': '712f4980-5ac1-41e5-9383-390aa7c9f58b'} +def _stub_qos_full(id, base_uri, tenant_id, name=None, specs=None): + if not name: + name = 'fake-name' + if not specs: + specs = {} + + return { + 'qos_specs': { + 'id': id, + 'name': name, + 'consumer': 'back-end', + 'specs': specs, + }, + 'links': { + 'href': _bookmark_href(base_uri, tenant_id, id), + 'rel': 'bookmark' + } + } + + +def _stub_qos_associates(id, name): + return { + 'assoications_type': 'volume_type', + 'name': name, + 'id': id, + } + + def _stub_transfer_full(id, base_uri, tenant_id): return { 'id': id, @@ -505,6 +533,68 @@ class FakeHTTPClient(base_client.HTTPClient): return (200, {}, {'restore': _stub_restore()}) + # + # QoSSpecs + # + + def get_qos_specs_1B6B6A04_A927_4AEB_810B_B7BAAD49F57C(self, **kw): + base_uri = 'http://localhost:8776' + tenant_id = '0fa851f6668144cf9cd8c8419c1646c1' + qos_id1 = '1B6B6A04-A927-4AEB-810B-B7BAAD49F57C' + return (200, {}, + _stub_qos_full(qos_id1, base_uri, tenant_id)) + + def get_qos_specs(self, **kw): + base_uri = 'http://localhost:8776' + tenant_id = '0fa851f6668144cf9cd8c8419c1646c1' + qos_id1 = '1B6B6A04-A927-4AEB-810B-B7BAAD49F57C' + qos_id2 = '0FD8DD14-A396-4E55-9573-1FE59042E95B' + return (200, {}, + {'qos_specs': [ + _stub_qos_full(qos_id1, base_uri, tenant_id, 'name-1'), + _stub_qos_full(qos_id2, base_uri, tenant_id)]}) + + def post_qos_specs(self, **kw): + base_uri = 'http://localhost:8776' + tenant_id = '0fa851f6668144cf9cd8c8419c1646c1' + qos_id = '1B6B6A04-A927-4AEB-810B-B7BAAD49F57C' + qos_name = 'qos-name' + return (202, {}, + _stub_qos_full(qos_id, base_uri, tenant_id, qos_name)) + + def put_qos_specs_1B6B6A04_A927_4AEB_810B_B7BAAD49F57C(self, **kw): + return (202, {}, None) + + def put_qos_specs_1B6B6A04_A927_4AEB_810B_B7BAAD49F57C_delete_keys( + self, **kw): + return (202, {}, None) + + def delete_qos_specs_1B6B6A04_A927_4AEB_810B_B7BAAD49F57C(self, **kw): + return (202, {}, None) + + def get_qos_specs_1B6B6A04_A927_4AEB_810B_B7BAAD49F57C_associations( + self, **kw): + type_id1 = '4230B13A-7A37-4E84-B777-EFBA6FCEE4FF' + type_id2 = '4230B13A-AB37-4E84-B777-EFBA6FCEE4FF' + type_name1 = 'type1' + type_name2 = 'type2' + return (202, {}, + {'qos_associations': [ + _stub_qos_associates(type_id1, type_name1), + _stub_qos_associates(type_id2, type_name2)]}) + + def get_qos_specs_1B6B6A04_A927_4AEB_810B_B7BAAD49F57C_associate( + self, **kw): + return (202, {}, None) + + def get_qos_specs_1B6B6A04_A927_4AEB_810B_B7BAAD49F57C_disassociate( + self, **kw): + return (202, {}, None) + + def get_qos_specs_1B6B6A04_A927_4AEB_810B_B7BAAD49F57C_disassociate_all( + self, **kw): + return (202, {}, None) + # # VolumeTransfers # diff --git a/cinderclient/tests/v1/test_qos.py b/cinderclient/tests/v1/test_qos.py new file mode 100644 index 0000000..e127a47 --- /dev/null +++ b/cinderclient/tests/v1/test_qos.py @@ -0,0 +1,79 @@ +# Copyright (C) 2013 eBay Inc. +# All Rights Reserved. +# +# 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 cinderclient.tests import utils +from cinderclient.tests.v1 import fakes + + +cs = fakes.FakeClient() + + +class QoSSpecsTest(utils.TestCase): + + def test_create(self): + specs = dict(k1='v1', k2='v2') + cs.qos_specs.create('qos-name', specs) + cs.assert_called('POST', '/qos-specs') + + def test_get(self): + qos_id = '1B6B6A04-A927-4AEB-810B-B7BAAD49F57C' + cs.qos_specs.get(qos_id) + cs.assert_called('GET', '/qos-specs/%s' % qos_id) + + def test_list(self): + cs.qos_specs.list() + cs.assert_called('GET', '/qos-specs') + + def test_delete(self): + cs.qos_specs.delete('1B6B6A04-A927-4AEB-810B-B7BAAD49F57C') + cs.assert_called('DELETE', + '/qos-specs/1B6B6A04-A927-4AEB-810B-B7BAAD49F57C?' + 'force=False') + + def test_set_keys(self): + body = {'qos_specs': dict(k1='v1')} + qos_id = '1B6B6A04-A927-4AEB-810B-B7BAAD49F57C' + cs.qos_specs.set_keys(qos_id, body) + cs.assert_called('PUT', '/qos-specs/%s' % qos_id) + + def test_unset_keys(self): + qos_id = '1B6B6A04-A927-4AEB-810B-B7BAAD49F57C' + body = {'keys': ['k1']} + cs.qos_specs.unset_keys(qos_id, body) + cs.assert_called('PUT', '/qos-specs/%s/delete_keys' % qos_id) + + def test_get_associations(self): + qos_id = '1B6B6A04-A927-4AEB-810B-B7BAAD49F57C' + cs.qos_specs.get_associations(qos_id) + cs.assert_called('GET', '/qos-specs/%s/associations' % qos_id) + + def test_associate(self): + qos_id = '1B6B6A04-A927-4AEB-810B-B7BAAD49F57C' + type_id = '4230B13A-7A37-4E84-B777-EFBA6FCEE4FF' + cs.qos_specs.associate(qos_id, type_id) + cs.assert_called('GET', '/qos-specs/%s/associate?vol_type_id=%s' + % (qos_id, type_id)) + + def test_disassociate(self): + qos_id = '1B6B6A04-A927-4AEB-810B-B7BAAD49F57C' + type_id = '4230B13A-7A37-4E84-B777-EFBA6FCEE4FF' + cs.qos_specs.disassociate(qos_id, type_id) + cs.assert_called('GET', '/qos-specs/%s/disassociate?vol_type_id=%s' + % (qos_id, type_id)) + + def test_disassociate_all(self): + qos_id = '1B6B6A04-A927-4AEB-810B-B7BAAD49F57C' + cs.qos_specs.disassociate_all(qos_id) + cs.assert_called('GET', '/qos-specs/%s/disassociate_all' % qos_id) diff --git a/cinderclient/tests/v2/fakes.py b/cinderclient/tests/v2/fakes.py index 88a94bb..d057456 100644 --- a/cinderclient/tests/v2/fakes.py +++ b/cinderclient/tests/v2/fakes.py @@ -119,6 +119,34 @@ def _stub_backup(id, base_uri, tenant_id): } +def _stub_qos_full(id, base_uri, tenant_id, name=None, specs=None): + if not name: + name = 'fake-name' + if not specs: + specs = {} + + return { + 'qos_specs': { + 'id': id, + 'name': name, + 'consumer': 'back-end', + 'specs': specs, + }, + 'links': { + 'href': _bookmark_href(base_uri, tenant_id, id), + 'rel': 'bookmark' + } + } + + +def _stub_qos_associates(id, name): + return { + 'assoications_type': 'volume_type', + 'name': name, + 'id': id, + } + + def _stub_restore(): return {'volume_id': '712f4980-5ac1-41e5-9383-390aa7c9f58b'} @@ -512,6 +540,69 @@ class FakeHTTPClient(base_client.HTTPClient): return (200, {}, {'restore': _stub_restore()}) + # + # QoSSpecs + # + + def get_qos_specs_1B6B6A04_A927_4AEB_810B_B7BAAD49F57C(self, **kw): + base_uri = 'http://localhost:8776' + tenant_id = '0fa851f6668144cf9cd8c8419c1646c1' + qos_id1 = '1B6B6A04-A927-4AEB-810B-B7BAAD49F57C' + return (200, {}, + _stub_qos_full(qos_id1, base_uri, tenant_id)) + + def get_qos_specs(self, **kw): + base_uri = 'http://localhost:8776' + tenant_id = '0fa851f6668144cf9cd8c8419c1646c1' + qos_id1 = '1B6B6A04-A927-4AEB-810B-B7BAAD49F57C' + qos_id2 = '0FD8DD14-A396-4E55-9573-1FE59042E95B' + return (200, {}, + {'qos_specs': [ + _stub_qos_full(qos_id1, base_uri, tenant_id, 'name-1'), + _stub_qos_full(qos_id2, base_uri, tenant_id)]}) + + def post_qos_specs(self, **kw): + base_uri = 'http://localhost:8776' + tenant_id = '0fa851f6668144cf9cd8c8419c1646c1' + qos_id = '1B6B6A04-A927-4AEB-810B-B7BAAD49F57C' + qos_name = 'qos-name' + return (202, {}, + _stub_qos_full(qos_id, base_uri, tenant_id, qos_name)) + + def put_qos_specs_1B6B6A04_A927_4AEB_810B_B7BAAD49F57C(self, **kw): + return (202, {}, None) + + def put_qos_specs_1B6B6A04_A927_4AEB_810B_B7BAAD49F57C_delete_keys( + self, **kw): + return (202, {}, None) + + def delete_qos_specs_1B6B6A04_A927_4AEB_810B_B7BAAD49F57C(self, **kw): + return (202, {}, None) + + def get_qos_specs_1B6B6A04_A927_4AEB_810B_B7BAAD49F57C_associations( + self, **kw): + type_id1 = '4230B13A-7A37-4E84-B777-EFBA6FCEE4FF' + type_id2 = '4230B13A-AB37-4E84-B777-EFBA6FCEE4FF' + type_name1 = 'type1' + type_name2 = 'type2' + return (202, {}, + {'qos_associations': [ + _stub_qos_associates(type_id1, type_name1), + _stub_qos_associates(type_id2, type_name2)]}) + + def get_qos_specs_1B6B6A04_A927_4AEB_810B_B7BAAD49F57C_associate( + self, **kw): + return (202, {}, None) + + def get_qos_specs_1B6B6A04_A927_4AEB_810B_B7BAAD49F57C_disassociate( + self, **kw): + return (202, {}, None) + + def get_qos_specs_1B6B6A04_A927_4AEB_810B_B7BAAD49F57C_disassociate_all( + self, **kw): + return (202, {}, None) + + # # # VolumeTransfers # diff --git a/cinderclient/tests/v2/test_qos.py b/cinderclient/tests/v2/test_qos.py new file mode 100644 index 0000000..3f3e6cf --- /dev/null +++ b/cinderclient/tests/v2/test_qos.py @@ -0,0 +1,79 @@ +# Copyright (C) 2013 eBay Inc. +# All Rights Reserved. +# +# 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 cinderclient.tests import utils +from cinderclient.tests.v2 import fakes + + +cs = fakes.FakeClient() + + +class QoSSpecsTest(utils.TestCase): + + def test_create(self): + specs = dict(k1='v1', k2='v2') + cs.qos_specs.create('qos-name', specs) + cs.assert_called('POST', '/qos-specs') + + def test_get(self): + qos_id = '1B6B6A04-A927-4AEB-810B-B7BAAD49F57C' + cs.qos_specs.get(qos_id) + cs.assert_called('GET', '/qos-specs/%s' % qos_id) + + def test_list(self): + cs.qos_specs.list() + cs.assert_called('GET', '/qos-specs') + + def test_delete(self): + cs.qos_specs.delete('1B6B6A04-A927-4AEB-810B-B7BAAD49F57C') + cs.assert_called('DELETE', + '/qos-specs/1B6B6A04-A927-4AEB-810B-B7BAAD49F57C?' + 'force=False') + + def test_set_keys(self): + body = {'qos_specs': dict(k1='v1')} + qos_id = '1B6B6A04-A927-4AEB-810B-B7BAAD49F57C' + cs.qos_specs.set_keys(qos_id, body) + cs.assert_called('PUT', '/qos-specs/%s' % qos_id) + + def test_unset_keys(self): + qos_id = '1B6B6A04-A927-4AEB-810B-B7BAAD49F57C' + body = {'keys': ['k1']} + cs.qos_specs.unset_keys(qos_id, body) + cs.assert_called('PUT', '/qos-specs/%s/delete_keys' % qos_id) + + def test_get_associations(self): + qos_id = '1B6B6A04-A927-4AEB-810B-B7BAAD49F57C' + cs.qos_specs.get_associations(qos_id) + cs.assert_called('GET', '/qos-specs/%s/associations' % qos_id) + + def test_associate(self): + qos_id = '1B6B6A04-A927-4AEB-810B-B7BAAD49F57C' + type_id = '4230B13A-7A37-4E84-B777-EFBA6FCEE4FF' + cs.qos_specs.associate(qos_id, type_id) + cs.assert_called('GET', '/qos-specs/%s/associate?vol_type_id=%s' + % (qos_id, type_id)) + + def test_disassociate(self): + qos_id = '1B6B6A04-A927-4AEB-810B-B7BAAD49F57C' + type_id = '4230B13A-7A37-4E84-B777-EFBA6FCEE4FF' + cs.qos_specs.disassociate(qos_id, type_id) + cs.assert_called('GET', '/qos-specs/%s/disassociate?vol_type_id=%s' + % (qos_id, type_id)) + + def test_disassociate_all(self): + qos_id = '1B6B6A04-A927-4AEB-810B-B7BAAD49F57C' + cs.qos_specs.disassociate_all(qos_id) + cs.assert_called('GET', '/qos-specs/%s/disassociate_all' % qos_id) diff --git a/cinderclient/v1/client.py b/cinderclient/v1/client.py index 4cbe6d0..82d1ee0 100644 --- a/cinderclient/v1/client.py +++ b/cinderclient/v1/client.py @@ -16,6 +16,7 @@ from cinderclient import client from cinderclient.v1 import availability_zones from cinderclient.v1 import limits +from cinderclient.v1 import qos_specs from cinderclient.v1 import quota_classes from cinderclient.v1 import quotas from cinderclient.v1 import services @@ -62,6 +63,7 @@ class Client(object): self.volume_types = volume_types.VolumeTypeManager(self) self.volume_encryption_types = \ volume_encryption_types.VolumeEncryptionTypeManager(self) + self.qos_specs = qos_specs.QoSSpecsManager(self) self.quota_classes = quota_classes.QuotaClassSetManager(self) self.quotas = quotas.QuotaSetManager(self) self.backups = volume_backups.VolumeBackupManager(self) diff --git a/cinderclient/v1/qos_specs.py b/cinderclient/v1/qos_specs.py new file mode 100644 index 0000000..b4e4272 --- /dev/null +++ b/cinderclient/v1/qos_specs.py @@ -0,0 +1,149 @@ +# Copyright (c) 2013 eBay Inc. +# Copyright (c) OpenStack LLC. +# +# 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. + + +""" +QoS Specs interface. +""" + +from cinderclient import base + + +class QoSSpecs(base.Resource): + """QoS specs entity represents quality-of-service parameters/requirements. + + A QoS specs is a set of parameters or requirements for quality-of-service + purpose, which can be associated with volume types (for now). In future, + QoS specs may be extended to be associated other entities, such as single + volume. + """ + def __repr__(self): + return "" % self.name + + def delete(self): + return self.manager.delete(self) + + +class QoSSpecsManager(base.ManagerWithFind): + """ + Manage :class:`QoSSpecs` resources. + """ + resource_class = QoSSpecs + + def list(self): + """Get a list of all qos specs. + + :rtype: list of :class:`QoSSpecs`. + """ + return self._list("/qos-specs", "qos_specs") + + def get(self, qos_specs): + """Get a specific qos specs. + + :param qos_specs: The ID of the :class:`QoSSpecs` to get. + :rtype: :class:`QoSSpecs` + """ + return self._get("/qos-specs/%s" % base.getid(qos_specs), "qos_specs") + + def delete(self, qos_specs, force=False): + """Delete a specific qos specs. + + :param qos_specs: The ID of the :class:`QoSSpecs` to be removed. + :param force: Flag that indicates whether to delete target qos specs + if it was in-use. + """ + self._delete("/qos-specs/%s?force=%s" % + (base.getid(qos_specs), force)) + + def create(self, name, specs): + """Create a qos specs. + + :param name: Descriptive name of the qos specs, must be unique + :param specs: A dict of key/value pairs to be set + :rtype: :class:`QoSSpecs` + """ + + body = { + "qos_specs": { + "name": name, + } + } + + body["qos_specs"].update(specs) + return self._create("/qos-specs", body, "qos_specs") + + def set_keys(self, qos_specs, specs): + """Update a qos specs with new specifications. + + :param qos_specs: The ID of qos specs + :param specs: A dict of key/value pairs to be set + :rtype: :class:`QoSSpecs` + """ + + body = { + "qos_specs": {} + } + + body["qos_specs"].update(specs) + return self._update("/qos-specs/%s" % qos_specs, body) + + def unset_keys(self, qos_specs, specs): + """Update a qos specs with new specifications. + + :param qos_specs: The ID of qos specs + :param specs: A list of key to be unset + :rtype: :class:`QoSSpecs` + """ + + body = {'keys': specs} + + return self._update("/qos-specs/%s/delete_keys" % qos_specs, + body) + + def get_associations(self, qos_specs): + """Get associated entities of a qos specs. + + :param qos_specs: The id of the :class: `QoSSpecs` + :return: a list of entities that associated with specific qos specs. + """ + return self._list("/qos-specs/%s/associations" % base.getid(qos_specs), + "qos_associations") + + def associate(self, qos_specs, vol_type_id): + """Associate a volume type with specific qos specs. + + :param qos_specs: The qos specs to be associated with + :param vol_type_id: The volume type id to be associated with + """ + self.api.client.get("/qos-specs/%s/associate?vol_type_id=%s" % + (base.getid(qos_specs), vol_type_id)) + + def disassociate(self, qos_specs, vol_type_id): + """Disassociate qos specs from volume type. + + :param qos_specs: The qos specs to be associated with + :param vol_type_id: The volume type id to be associated with + """ + self.api.client.get("/qos-specs/%s/disassociate?vol_type_id=%s" % + (base.getid(qos_specs), vol_type_id)) + + def disassociate_all(self, qos_specs): + """Disassociate all entities from specific qos specs. + + :param qos_specs: The qos specs to be associated with + """ + self.api.client.get("/qos-specs/%s/disassociate_all" % + base.getid(qos_specs)) diff --git a/cinderclient/v1/shell.py b/cinderclient/v1/shell.py index fbd76bd..afb5f85 100644 --- a/cinderclient/v1/shell.py +++ b/cinderclient/v1/shell.py @@ -24,6 +24,7 @@ import sys import time from cinderclient import exceptions +from cinderclient.openstack.common import strutils from cinderclient import utils from cinderclient.v1 import availability_zones @@ -75,6 +76,11 @@ def _find_transfer(cs, transfer): return utils.find_resource(cs.transfers, transfer) +def _find_qos_specs(cs, qos_specs): + """Get a qos specs by ID.""" + return utils.find_resource(cs.qos_specs, qos_specs) + + def _print_volume(volume): utils.print_dict(volume._info) @@ -1068,3 +1074,125 @@ def do_migrate(cs, args): """Migrate the volume to the new host.""" volume = _find_volume(cs, args.volume) volume.migrate_volume(args.host, args.force_host_copy) + + +def _print_qos_specs(qos_specs): + utils.print_dict(qos_specs._info) + + +def _print_qos_specs_list(q_specs): + utils.print_list(q_specs, ['ID', 'Name', 'Consumer', 'specs']) + + +def _print_qos_specs_and_associations_list(q_specs): + utils.print_list(q_specs, ['ID', 'Name', 'Consumer', 'specs']) + + +def _print_associations_list(associations): + utils.print_list(associations, ['Association_Type', 'Name', 'ID']) + + +@utils.arg('name', + metavar='', + help="Name of the new QoS specs") +@utils.arg('metadata', + metavar='', + nargs='+', + default=[], + help='Specifications for QoS') +@utils.service_type('volume') +def do_qos_create(cs, args): + """Create a new qos specs.""" + keypair = None + if args.metadata is not None: + keypair = _extract_metadata(args) + qos_specs = cs.qos_specs.create(args.name, keypair) + _print_qos_specs(qos_specs) + + +@utils.service_type('volume') +def do_qos_list(cs, args): + """Get full list of qos specs.""" + qos_specs = cs.qos_specs.list() + _print_qos_specs_list(qos_specs) + + +@utils.arg('qos_specs', metavar='', + help='ID of the qos_specs to show.') +@utils.service_type('volume') +def do_qos_show(cs, args): + """Get a specific qos specs.""" + qos_specs = _find_qos_specs(cs, args.qos_specs) + _print_qos_specs(qos_specs) + + +@utils.arg('qos_specs', metavar='', + help='ID of the qos_specs to delete.') +@utils.arg('--force', + metavar='', + default=False, + help='Optional flag that indicates whether to delete ' + 'specified qos specs even if it is in-use.') +@utils.service_type('volume') +def do_qos_delete(cs, args): + """Delete a specific qos specs.""" + force = strutils.bool_from_string(args.force) + qos_specs = _find_qos_specs(cs, args.qos_specs) + cs.qos_specs.delete(qos_specs, force) + + +@utils.arg('qos_specs', metavar='', + help='ID of qos_specs.') +@utils.arg('vol_type_id', metavar='', + help='ID of volume type to be associated with.') +@utils.service_type('volume') +def do_qos_associate(cs, args): + """Associate qos specs with specific volume type.""" + cs.qos_specs.associate(args.qos_specs, args.vol_type_id) + + +@utils.arg('qos_specs', metavar='', + help='ID of qos_specs.') +@utils.arg('vol_type_id', metavar='', + help='ID of volume type to be associated with.') +@utils.service_type('volume') +def do_qos_disassociate(cs, args): + """Disassociate qos specs from specific volume type.""" + cs.qos_specs.disassociate(args.qos_specs, args.vol_type_id) + + +@utils.arg('qos_specs', metavar='', + help='ID of qos_specs to be operate on.') +@utils.service_type('volume') +def do_qos_disassociate_all(cs, args): + """Disassociate qos specs from all of its associations.""" + cs.qos_specs.disassociate_all(args.qos_specs) + + +@utils.arg('qos_specs', metavar='', + help='ID of qos specs') +@utils.arg('action', + metavar='', + choices=['set', 'unset'], + help="Actions: 'set' or 'unset'") +@utils.arg('metadata', metavar='key=value', + nargs='+', + default=[], + help='QoS specs to set/unset (only key is necessary on unset)') +def do_qos_key(cs, args): + """Set or unset specifications for a qos spec.""" + keypair = _extract_metadata(args) + + if args.action == 'set': + cs.qos_specs.set_keys(args.qos_specs, keypair) + elif args.action == 'unset': + cs.qos_specs.unset_keys(args.qos_specs, list(keypair.keys())) + + +@utils.arg('qos_specs', metavar='', + help='ID of the qos_specs.') +@utils.service_type('volume') +def do_qos_get_association(cs, args): + """Get all associations of specific qos specs.""" + associations = cs.qos_specs.get_associations(args.qos_specs) + _print_associations_list(associations) diff --git a/cinderclient/v2/client.py b/cinderclient/v2/client.py index 9b8cad9..7b91c23 100644 --- a/cinderclient/v2/client.py +++ b/cinderclient/v2/client.py @@ -16,6 +16,7 @@ from cinderclient import client from cinderclient.v1 import availability_zones from cinderclient.v2 import limits +from cinderclient.v2 import qos_specs from cinderclient.v2 import quota_classes from cinderclient.v2 import quotas from cinderclient.v2 import services @@ -60,6 +61,7 @@ class Client(object): self.volume_types = volume_types.VolumeTypeManager(self) self.volume_encryption_types = \ volume_encryption_types.VolumeEncryptionTypeManager(self) + self.qos_specs = qos_specs.QoSSpecsManager(self) self.quota_classes = quota_classes.QuotaClassSetManager(self) self.quotas = quotas.QuotaSetManager(self) self.backups = volume_backups.VolumeBackupManager(self) diff --git a/cinderclient/v2/qos_specs.py b/cinderclient/v2/qos_specs.py new file mode 100644 index 0000000..b4e4272 --- /dev/null +++ b/cinderclient/v2/qos_specs.py @@ -0,0 +1,149 @@ +# Copyright (c) 2013 eBay Inc. +# Copyright (c) OpenStack LLC. +# +# 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. + + +""" +QoS Specs interface. +""" + +from cinderclient import base + + +class QoSSpecs(base.Resource): + """QoS specs entity represents quality-of-service parameters/requirements. + + A QoS specs is a set of parameters or requirements for quality-of-service + purpose, which can be associated with volume types (for now). In future, + QoS specs may be extended to be associated other entities, such as single + volume. + """ + def __repr__(self): + return "" % self.name + + def delete(self): + return self.manager.delete(self) + + +class QoSSpecsManager(base.ManagerWithFind): + """ + Manage :class:`QoSSpecs` resources. + """ + resource_class = QoSSpecs + + def list(self): + """Get a list of all qos specs. + + :rtype: list of :class:`QoSSpecs`. + """ + return self._list("/qos-specs", "qos_specs") + + def get(self, qos_specs): + """Get a specific qos specs. + + :param qos_specs: The ID of the :class:`QoSSpecs` to get. + :rtype: :class:`QoSSpecs` + """ + return self._get("/qos-specs/%s" % base.getid(qos_specs), "qos_specs") + + def delete(self, qos_specs, force=False): + """Delete a specific qos specs. + + :param qos_specs: The ID of the :class:`QoSSpecs` to be removed. + :param force: Flag that indicates whether to delete target qos specs + if it was in-use. + """ + self._delete("/qos-specs/%s?force=%s" % + (base.getid(qos_specs), force)) + + def create(self, name, specs): + """Create a qos specs. + + :param name: Descriptive name of the qos specs, must be unique + :param specs: A dict of key/value pairs to be set + :rtype: :class:`QoSSpecs` + """ + + body = { + "qos_specs": { + "name": name, + } + } + + body["qos_specs"].update(specs) + return self._create("/qos-specs", body, "qos_specs") + + def set_keys(self, qos_specs, specs): + """Update a qos specs with new specifications. + + :param qos_specs: The ID of qos specs + :param specs: A dict of key/value pairs to be set + :rtype: :class:`QoSSpecs` + """ + + body = { + "qos_specs": {} + } + + body["qos_specs"].update(specs) + return self._update("/qos-specs/%s" % qos_specs, body) + + def unset_keys(self, qos_specs, specs): + """Update a qos specs with new specifications. + + :param qos_specs: The ID of qos specs + :param specs: A list of key to be unset + :rtype: :class:`QoSSpecs` + """ + + body = {'keys': specs} + + return self._update("/qos-specs/%s/delete_keys" % qos_specs, + body) + + def get_associations(self, qos_specs): + """Get associated entities of a qos specs. + + :param qos_specs: The id of the :class: `QoSSpecs` + :return: a list of entities that associated with specific qos specs. + """ + return self._list("/qos-specs/%s/associations" % base.getid(qos_specs), + "qos_associations") + + def associate(self, qos_specs, vol_type_id): + """Associate a volume type with specific qos specs. + + :param qos_specs: The qos specs to be associated with + :param vol_type_id: The volume type id to be associated with + """ + self.api.client.get("/qos-specs/%s/associate?vol_type_id=%s" % + (base.getid(qos_specs), vol_type_id)) + + def disassociate(self, qos_specs, vol_type_id): + """Disassociate qos specs from volume type. + + :param qos_specs: The qos specs to be associated with + :param vol_type_id: The volume type id to be associated with + """ + self.api.client.get("/qos-specs/%s/disassociate?vol_type_id=%s" % + (base.getid(qos_specs), vol_type_id)) + + def disassociate_all(self, qos_specs): + """Disassociate all entities from specific qos specs. + + :param qos_specs: The qos specs to be associated with + """ + self.api.client.get("/qos-specs/%s/disassociate_all" % + base.getid(qos_specs)) diff --git a/cinderclient/v2/shell.py b/cinderclient/v2/shell.py index 509b5a4..c1eb0c4 100644 --- a/cinderclient/v2/shell.py +++ b/cinderclient/v2/shell.py @@ -25,6 +25,7 @@ import six from cinderclient import exceptions from cinderclient import utils +from cinderclient.openstack.common import strutils from cinderclient.v2 import availability_zones @@ -73,6 +74,11 @@ def _find_transfer(cs, transfer): return utils.find_resource(cs.transfers, transfer) +def _find_qos_specs(cs, qos_specs): + """Get a qos specs by ID.""" + return utils.find_resource(cs.qos_specs, qos_specs) + + def _print_volume_snapshot(snapshot): utils.print_dict(snapshot._info) @@ -106,7 +112,7 @@ def _translate_availability_zone_keys(collection): def _extract_metadata(args): metadata = {} - for metadatum in args.metadata[0]: + for metadatum in args.metadata: # unset doesn't require a val, so we have the if/else if '=' in metadatum: (key, value) = metadatum.split('=', 1) @@ -369,7 +375,6 @@ def do_rename(cs, args): @utils.arg('metadata', metavar='', nargs='+', - action='append', default=[], help='Metadata to set/unset (only key is necessary on unset)') @utils.service_type('volumev2') @@ -592,12 +597,11 @@ def do_type_delete(cs, args): @utils.arg('metadata', metavar='', nargs='+', - action='append', default=[], help='Extra_specs to set/unset (only key is necessary on unset)') @utils.service_type('volumev2') def do_type_key(cs, args): - "Set or unset extra_spec for a volume type.""" + """Set or unset extra_spec for a volume type.""" vtype = _find_volume_type(cs, args.vtype) keypair = _extract_metadata(args) @@ -1148,3 +1152,125 @@ def do_encryption_type_create(cs, args): result = cs.volume_encryption_types.create(volume_type, body) _print_volume_encryption_type_list([result]) + + +def _print_qos_specs(qos_specs): + utils.print_dict(qos_specs._info) + + +def _print_qos_specs_list(q_specs): + utils.print_list(q_specs, ['ID', 'Name', 'Consumer', 'specs']) + + +def _print_qos_specs_and_associations_list(q_specs): + utils.print_list(q_specs, ['ID', 'Name', 'Consumer', 'specs']) + + +def _print_associations_list(associations): + utils.print_list(associations, ['Association_Type', 'Name', 'ID']) + + +@utils.arg('name', + metavar='', + help="Name of the new QoS specs") +@utils.arg('metadata', + metavar='', + nargs='+', + default=[], + help='Specifications for QoS') +@utils.service_type('volumev2') +def do_qos_create(cs, args): + """Create a new qos specs.""" + keypair = None + if args.metadata is not None: + keypair = _extract_metadata(args) + qos_specs = cs.qos_specs.create(args.name, keypair) + _print_qos_specs(qos_specs) + + +@utils.service_type('volumev2') +def do_qos_list(cs, args): + """Get full list of qos specs.""" + qos_specs = cs.qos_specs.list() + _print_qos_specs_list(qos_specs) + + +@utils.arg('qos_specs', metavar='', + help='ID of the qos_specs to show.') +@utils.service_type('volumev2') +def do_qos_show(cs, args): + """Get a specific qos specs.""" + qos_specs = _find_qos_specs(cs, args.qos_specs) + _print_qos_specs(qos_specs) + + +@utils.arg('qos_specs', metavar='', + help='ID of the qos_specs to delete.') +@utils.arg('--force', + metavar='', + default=False, + help='Optional flag that indicates whether to delete ' + 'specified qos specs even if it is in-use.') +@utils.service_type('volumev2') +def do_qos_delete(cs, args): + """Delete a specific qos specs.""" + force = strutils.bool_from_string(args.force) + qos_specs = _find_qos_specs(cs, args.qos_specs) + cs.qos_specs.delete(qos_specs, force) + + +@utils.arg('qos_specs', metavar='', + help='ID of qos_specs.') +@utils.arg('vol_type_id', metavar='', + help='ID of volume type to be associated with.') +@utils.service_type('volumev2') +def do_qos_associate(cs, args): + """Associate qos specs with specific volume type.""" + cs.qos_specs.associate(args.qos_specs, args.vol_type_id) + + +@utils.arg('qos_specs', metavar='', + help='ID of qos_specs.') +@utils.arg('vol_type_id', metavar='', + help='ID of volume type to be associated with.') +@utils.service_type('volumev2') +def do_qos_disassociate(cs, args): + """Disassociate qos specs from specific volume type.""" + cs.qos_specs.disassociate(args.qos_specs, args.vol_type_id) + + +@utils.arg('qos_specs', metavar='', + help='ID of qos_specs to be operate on.') +@utils.service_type('volumev2') +def do_qos_disassociate_all(cs, args): + """Disassociate qos specs from all of its associations.""" + cs.qos_specs.disassociate_all(args.qos_specs) + + +@utils.arg('qos_specs', metavar='', + help='ID of qos specs') +@utils.arg('action', + metavar='', + choices=['set', 'unset'], + help="Actions: 'set' or 'unset'") +@utils.arg('metadata', metavar='key=value', + nargs='+', + default=[], + help='QoS specs to set/unset (only key is necessary on unset)') +def do_qos_key(cs, args): + """Set or unset specifications for a qos spec.""" + keypair = _extract_metadata(args) + + if args.action == 'set': + cs.qos_specs.set_keys(args.qos_specs, keypair) + elif args.action == 'unset': + cs.qos_specs.unset_keys(args.qos_specs, list(keypair.keys())) + + +@utils.arg('qos_specs', metavar='', + help='ID of the qos_specs.') +@utils.service_type('volumev2') +def do_qos_get_association(cs, args): + """Get all associations of specific qos specs.""" + associations = cs.qos_specs.get_associations(args.qos_specs) + _print_associations_list(associations)