diff --git a/cinderclient/v1/client.py b/cinderclient/v1/client.py index d5f982d65..e8e1207c2 100644 --- a/cinderclient/v1/client.py +++ b/cinderclient/v1/client.py @@ -17,9 +17,7 @@ class Client(object): Then call methods on its managers:: - >>> client.servers.list() - ... - >>> client.flavors.list() + >>> client.volumes.list() ... """ diff --git a/cinderclient/v1/shell.py b/cinderclient/v1/shell.py index 7a202f155..ea64eb39a 100644 --- a/cinderclient/v1/shell.py +++ b/cinderclient/v1/shell.py @@ -20,6 +20,7 @@ import os import sys import time +from cinderclient import exceptions from cinderclient import utils @@ -91,14 +92,17 @@ def _translate_volume_snapshot_keys(collection): setattr(item, to_key, item._info[from_key]) -def _extract_metadata(arg_list): +def _extract_metadata(args): metadata = {} - for metadatum in arg_list: - assert(metadatum.find('=') > -1), "Improperly formatted metadata "\ - "input (%s)" % metadatum - (key, value) = metadatum.split('=', 1) - metadata[key] = value + for metadatum in args.metadata[0]: + # unset doesn't require a val, so we have the if/else + if '=' in metadatum: + (key, value) = metadatum.split('=', 1) + else: + key = metadatum + value = None + metadata[key] = value return metadata @@ -219,7 +223,7 @@ def do_create(cs, args): volume_metadata = None if args.metadata is not None: - volume_metadata = _extract_metadata(args.metadata) + volume_metadata = _extract_metadata(args) volume = cs.volumes.create(args.size, args.snapshot_id, @@ -375,7 +379,9 @@ def do_snapshot_rename(cs, args): def _print_volume_type_list(vtypes): - utils.print_list(vtypes, ['ID', 'Name']) + #_translate_type_keys(vtypes) + formatters = {'extra_specs': _print_type_extra_specs} + utils.print_list(vtypes, ['ID', 'Name', 'extra_specs'], formatters) @utils.service_type('volume') @@ -387,7 +393,7 @@ def do_type_list(cs, args): @utils.arg('name', metavar='', - help="Name of the new flavor") + help="Name of the new volume type") @utils.service_type('volume') def do_type_create(cs, args): """Create a new volume type.""" @@ -400,10 +406,35 @@ def do_type_create(cs, args): help="Unique ID of the volume type to delete") @utils.service_type('volume') def do_type_delete(cs, args): - """Delete a specific flavor""" + """Delete a specific volume type""" cs.volume_types.delete(args.id) +@utils.arg('vtype', + metavar='', + help="Name or ID of the volume type") +@utils.arg('action', + metavar='', + choices=['set', 'unset'], + help="Actions: 'set' or 'unset'") +@utils.arg('metadata', + metavar='', + nargs='+', + action='append', + default=[], + help='Extra_specs to set/unset (only key is necessary on unset)') +@utils.service_type('volume') +def do_type_key(cs, args): + "Set or unset extra_spec for a volume type.""" + vtype = _find_volume_type(cs, args.vtype) + keypair = _extract_metadata(args) + + if args.action == 'set': + vtype.set_keys(keypair) + elif args.action == 'unset': + vtype.unset_keys(keypair.keys()) + + def do_endpoints(cs, args): """Discover endpoints that get returned from the authenticate services""" catalog = cs.client.service_catalog.catalog @@ -513,3 +544,15 @@ def do_rate_limits(cs, args): limits = cs.limits.get().rate columns = ['Verb', 'URI', 'Value', 'Remain', 'Unit', 'Next_Available'] utils.print_list(limits, columns) + + +def _print_type_extra_specs(vol_type): + try: + return vol_type.get_keys() + except exceptions.NotFound: + return "N/A" + + +def _find_volume_type(cs, vtype): + """Get a volume type by name or ID.""" + return utils.find_resource(cs.volume_types, vtype) diff --git a/cinderclient/v1/volume_types.py b/cinderclient/v1/volume_types.py index e6d644df4..f23078652 100644 --- a/cinderclient/v1/volume_types.py +++ b/cinderclient/v1/volume_types.py @@ -26,7 +26,52 @@ class VolumeType(base.Resource): A Volume Type is the type of volume to be created """ def __repr__(self): - return "" % self.name + return "" % self.name + + def get_keys(self): + """ + Get extra specs from a volume type. + + :param vol_type: The :class:`VolumeType` to get extra specs from + """ + _resp, body = self.manager.api.client.get( + "/types/%s/extra_specs" % + base.getid(self)) + return body["extra_specs"] + + def set_keys(self, metadata): + """ + Set extra specs on a volume type. + + :param type : The :class:`VolumeType` to set extra spec on + :param metadata: A dict of key/value pairs to be set + """ + body = {'extra_specs': metadata} + return self.manager._create( + "/types/%s/extra_specs" % base.getid(self), + body, + "extra_specs", + return_raw=True) + + def unset_keys(self, keys): + """ + Unset extra specs on a volue type. + + :param type_id: The :class:`VolumeType` to unset extra spec on + :param keys: A list of keys to be unset + """ + + # NOTE(jdg): This wasn't actually doing all of the keys before + # the return in the loop resulted in ony ONE key being unset. + # since on success the return was NONE, we'll only interrupt the loop + # and return if there's an error + result = None + for k in keys: + resp = self.manager._delete( + "/types/%s/extra_specs/%s" % ( + base.getid(self), k)) + if resp is not None: + return resp class VolumeTypeManager(base.ManagerWithFind): diff --git a/tests/v1/fakes.py b/tests/v1/fakes.py index de6c552a0..6dd2bec9f 100644 --- a/tests/v1/fakes.py +++ b/tests/v1/fakes.py @@ -227,3 +227,35 @@ class FakeHTTPClient(base_client.HTTPClient): 'metadata_items': [], 'volumes': 2, 'gigabytes': 1}}) + + # + # VolumeTypes + # + def get_types(self, **kw): + return (200, { + 'volume_types': [{'id': 1, + 'name': 'test-type-1', + 'extra_specs':{}}, + {'id': 2, + 'name': 'test-type-2', + 'extra_specs':{}}]}) + + def get_types_1(self, **kw): + return (200, {'volume_type': {'id': 1, + 'name': 'test-type-1', + 'extra_specs': {}}}) + + def post_types(self, body, **kw): + return (202, {'volume_type': {'id': 3, + 'name': 'test-type-3', + 'extra_specs': {}}}) + + def post_types_1_extra_specs(self, body, **kw): + assert body.keys() == ['extra_specs'] + return (200, {'extra_specs': {'k': 'v'}}) + + def delete_types_1_extra_specs_k(self, **kw): + return(204, None) + + def delete_types_1(self, **kw): + return (202, None) diff --git a/tests/v1/test_types.py b/tests/v1/test_types.py new file mode 100644 index 000000000..92aa2c090 --- /dev/null +++ b/tests/v1/test_types.py @@ -0,0 +1,35 @@ +from cinderclient import exceptions +from cinderclient.v1 import volume_types +from tests import utils +from tests.v1 import fakes + +cs = fakes.FakeClient() + + +class TypesTest(utils.TestCase): + def test_list_types(self): + tl = cs.volume_types.list() + cs.assert_called('GET', '/types') + for t in tl: + self.assertTrue(isinstance(t, volume_types.VolumeType)) + + def test_create(self): + t = cs.volume_types.create('test-type-3') + cs.assert_called('POST', '/types') + self.assertTrue(isinstance(t, volume_types.VolumeType)) + + def test_set_key(self): + t = cs.volume_types.get(1) + t.set_keys({'k': 'v'}) + cs.assert_called('POST', + '/types/1/extra_specs', + {'extra_specs': {'k': 'v'}}) + + def test_unsset_keys(self): + t = cs.volume_types.get(1) + t.unset_keys(['k']) + cs.assert_called('DELETE', '/types/1/extra_specs/k') + + def test_delete(self): + cs.volume_types.delete(1) + cs.assert_called('DELETE', '/types/1')