diff --git a/README.rst b/README.rst index 8ad1fabe2..241f75050 100644 --- a/README.rst +++ b/README.rst @@ -80,12 +80,14 @@ You'll find complete documentation on the shell by running Positional arguments: + absolute-limits Print a list of absolute limits for a user create Add a new volume. credentials Show user credentials returned from auth delete Remove a volume. endpoints Discover endpoints that get returned from the authenticate services list List all the volumes. + rate-limits Print a list of rate limits for a user show Show details about a volume. snapshot-create Add a new snapshot. snapshot-delete Remove a snapshot. diff --git a/cinderclient/v1/client.py b/cinderclient/v1/client.py index a8d405ceb..bf008445d 100644 --- a/cinderclient/v1/client.py +++ b/cinderclient/v1/client.py @@ -1,4 +1,7 @@ from cinderclient import client +from cinderclient.v1 import limits +from cinderclient.v1 import quota_classes +from cinderclient.v1 import quotas from cinderclient.v1 import volumes from cinderclient.v1 import volume_snapshots from cinderclient.v1 import volume_types @@ -6,7 +9,7 @@ from cinderclient.v1 import volume_types class Client(object): """ - Top-level object to access the OpenStack Compute API. + Top-level object to access the OpenStack Volume API. Create an instance with your creds:: @@ -26,16 +29,19 @@ class Client(object): insecure=False, timeout=None, proxy_tenant_id=None, proxy_token=None, region_name=None, endpoint_type='publicURL', extensions=None, - service_type='compute', service_name=None, + service_type='volume', service_name=None, volume_service_name=None): # FIXME(comstud): Rename the api_key argument above when we # know it's not being used as keyword argument password = api_key + self.limits = limits.LimitsManager(self) # extensions self.volumes = volumes.VolumeManager(self) self.volume_snapshots = volume_snapshots.SnapshotManager(self) self.volume_types = volume_types.VolumeTypeManager(self) + self.quota_classes = quota_classes.QuotaClassSetManager(self) + self.quotas = quotas.QuotaSetManager(self) # Add in any extensions... if extensions: diff --git a/cinderclient/v1/limits.py b/cinderclient/v1/limits.py new file mode 100644 index 000000000..2008a6909 --- /dev/null +++ b/cinderclient/v1/limits.py @@ -0,0 +1,79 @@ +# Copyright 2011 OpenStack LLC. + +from cinderclient import base + + +class Limits(base.Resource): + """A collection of RateLimit and AbsoluteLimit objects""" + + def __repr__(self): + return "" + + @property + def absolute(self): + for (name, value) in self._info['absolute'].items(): + yield AbsoluteLimit(name, value) + + @property + def rate(self): + for group in self._info['rate']: + uri = group['uri'] + regex = group['regex'] + for rate in group['limit']: + yield RateLimit(rate['verb'], uri, regex, rate['value'], + rate['remaining'], rate['unit'], + rate['next-available']) + + +class RateLimit(object): + """Data model that represents a flattened view of a single rate limit""" + + def __init__(self, verb, uri, regex, value, remain, + unit, next_available): + self.verb = verb + self.uri = uri + self.regex = regex + self.value = value + self.remain = remain + self.unit = unit + self.next_available = next_available + + def __eq__(self, other): + return self.uri == other.uri \ + and self.regex == other.regex \ + and self.value == other.value \ + and self.verb == other.verb \ + and self.remain == other.remain \ + and self.unit == other.unit \ + and self.next_available == other.next_available + + def __repr__(self): + return "" % (self.method, self.uri) + + +class AbsoluteLimit(object): + """Data model that represents a single absolute limit""" + + def __init__(self, name, value): + self.name = name + self.value = value + + def __eq__(self, other): + return self.value == other.value and self.name == other.name + + def __repr__(self): + return "" % (self.name) + + +class LimitsManager(base.Manager): + """Manager object used to interact with limits resource""" + + resource_class = Limits + + def get(self): + """ + Get a specific extension. + + :rtype: :class:`Limits` + """ + return self._get("/limits", "limits") diff --git a/cinderclient/v1/quota_classes.py b/cinderclient/v1/quota_classes.py new file mode 100644 index 000000000..6aa4fdc40 --- /dev/null +++ b/cinderclient/v1/quota_classes.py @@ -0,0 +1,52 @@ +# Copyright 2012 OpenStack LLC. +# 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 import base + + +class QuotaClassSet(base.Resource): + + @property + def id(self): + """QuotaClassSet does not have a 'id' attribute but base.Resource + needs it to self-refresh and QuotaSet is indexed by class_name""" + return self.class_name + + def update(self, *args, **kwargs): + self.manager.update(self.class_name, *args, **kwargs) + + +class QuotaClassSetManager(base.ManagerWithFind): + resource_class = QuotaClassSet + + def get(self, class_name): + return self._get("/os-quota-class-sets/%s" % (class_name), + "quota_class_set") + + def update(self, + class_name, + volumes=None, + gigabytes=None): + + body = {'quota_class_set': { + 'class_name': class_name, + 'volumes': volumes, + 'gigabytes': gigabytes}} + + for key in body['quota_class_set'].keys(): + if body['quota_class_set'][key] is None: + body['quota_class_set'].pop(key) + + self._update('/os-quota-class-sets/%s' % (class_name), body) diff --git a/cinderclient/v1/quotas.py b/cinderclient/v1/quotas.py new file mode 100644 index 000000000..c998f0aa4 --- /dev/null +++ b/cinderclient/v1/quotas.py @@ -0,0 +1,54 @@ +# Copyright 2011 OpenStack LLC. +# 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 import base + + +class QuotaSet(base.Resource): + + @property + def id(self): + """QuotaSet does not have a 'id' attribute but base.Resource needs it + to self-refresh and QuotaSet is indexed by tenant_id""" + return self.tenant_id + + def update(self, *args, **kwargs): + self.manager.update(self.tenant_id, *args, **kwargs) + + +class QuotaSetManager(base.ManagerWithFind): + resource_class = QuotaSet + + def get(self, tenant_id): + if hasattr(tenant_id, 'tenant_id'): + tenant_id = tenant_id.tenant_id + return self._get("/os-quota-sets/%s" % (tenant_id), "quota_set") + + def update(self, tenant_id, volumes=None, gigabytes=None): + + body = {'quota_set': { + 'tenant_id': tenant_id, + 'volumes': volumes, + 'gigabytes': gigabytes}} + + for key in body['quota_set'].keys(): + if body['quota_set'][key] is None: + body['quota_set'].pop(key) + + self._update('/os-quota-sets/%s' % (tenant_id), body) + + def defaults(self, tenant_id): + return self._get('/os-quota-sets/%s/defaults' % tenant_id, + 'quota_set') diff --git a/cinderclient/v1/shell.py b/cinderclient/v1/shell.py index 69bed6c48..1983c48ce 100644 --- a/cinderclient/v1/shell.py +++ b/cinderclient/v1/shell.py @@ -346,3 +346,100 @@ def do_credentials(cs, args): catalog = cs.client.service_catalog.catalog utils.print_dict(catalog['access']['user'], "User Credentials") utils.print_dict(catalog['access']['token'], "Token") + +_quota_resources = ['volumes', 'gigabytes'] + + +def _quota_show(quotas): + quota_dict = {} + for resource in _quota_resources: + quota_dict[resource] = getattr(quotas, resource, None) + utils.print_dict(quota_dict) + + +def _quota_update(manager, identifier, args): + updates = {} + for resource in _quota_resources: + val = getattr(args, resource, None) + if val is not None: + updates[resource] = val + + if updates: + manager.update(identifier, **updates) + + +@utils.arg('tenant', metavar='', + help='UUID of tenant to list the quotas for.') +@utils.service_type('volume') +def do_quota_show(cs, args): + """List the quotas for a tenant.""" + + _quota_show(cs.quotas.get(args.tenant)) + + +@utils.arg('tenant', metavar='', + help='UUID of tenant to list the default quotas for.') +@utils.service_type('volume') +def do_quota_defaults(cs, args): + """List the default quotas for a tenant.""" + + _quota_show(cs.quotas.defaults(args.tenant)) + + +@utils.arg('tenant', metavar='', + help='UUID of tenant to set the quotas for.') +@utils.arg('--volumes', + metavar='', + type=int, default=None, + help='New value for the "volumes" quota.') +@utils.arg('--gigabytes', + metavar='', + type=int, default=None, + help='New value for the "gigabytes" quota.') +@utils.service_type('volume') +def do_quota_update(cs, args): + """Update the quotas for a tenant.""" + + _quota_update(cs.quotas, args.tenant, args) + + +@utils.arg('class_name', metavar='', + help='Name of quota class to list the quotas for.') +@utils.service_type('volume') +def do_quota_class_show(cs, args): + """List the quotas for a quota class.""" + + _quota_show(cs.quota_classes.get(args.class_name)) + + +@utils.arg('class_name', metavar='', + help='Name of quota class to set the quotas for.') +@utils.arg('--volumes', + metavar='', + type=int, default=None, + help='New value for the "volumes" quota.') +@utils.arg('--gigabytes', + metavar='', + type=int, default=None, + help='New value for the "gigabytes" quota.') +@utils.service_type('volume') +def do_quota_class_update(cs, args): + """Update the quotas for a quota class.""" + + _quota_update(cs.quota_classes, args.class_name, args) + + +@utils.service_type('volume') +def do_absolute_limits(cs, args): + """Print a list of absolute limits for a user""" + limits = cs.limits.get().absolute + columns = ['Name', 'Value'] + utils.print_list(limits, columns) + + +@utils.service_type('volume') +def do_rate_limits(cs, args): + """Print a list of rate limits for a user""" + limits = cs.limits.get().rate + columns = ['Verb', 'URI', 'Value', 'Remain', 'Unit', 'Next_Available'] + utils.print_list(limits, columns) diff --git a/tests/v1/test_limits.py b/tests/v1/test_limits.py new file mode 100644 index 000000000..324af8ae0 --- /dev/null +++ b/tests/v1/test_limits.py @@ -0,0 +1,52 @@ + +from cinderclient.v1 import limits +from tests import utils +from tests.v1 import fakes + + +cs = fakes.FakeClient() + + +class LimitsTest(utils.TestCase): + + def test_get_limits(self): + obj = cs.limits.get() + cs.assert_called('GET', '/limits') + self.assertTrue(isinstance(obj, limits.Limits)) + + def test_absolute_limits(self): + obj = cs.limits.get() + + expected = ( + limits.AbsoluteLimit("maxTotalRAMSize", 51200), + limits.AbsoluteLimit("maxServerMeta", 5), + limits.AbsoluteLimit("maxImageMeta", 5), + limits.AbsoluteLimit("maxPersonality", 5), + limits.AbsoluteLimit("maxPersonalitySize", 10240), + ) + + abs_limits = list(obj.absolute) + self.assertEqual(len(abs_limits), len(expected)) + + for limit in abs_limits: + self.assertTrue(limit in expected) + + def test_rate_limits(self): + obj = cs.limits.get() + + expected = ( + limits.RateLimit('POST', '*', '.*', 10, 2, 'MINUTE', + '2011-12-15T22:42:45Z'), + limits.RateLimit('PUT', '*', '.*', 10, 2, 'MINUTE', + '2011-12-15T22:42:45Z'), + limits.RateLimit('DELETE', '*', '.*', 100, 100, 'MINUTE', + '2011-12-15T22:42:45Z'), + limits.RateLimit('POST', '*/servers', '^/servers', 25, 24, 'DAY', + '2011-12-15T22:42:45Z'), + ) + + rate_limits = list(obj.rate) + self.assertEqual(len(rate_limits), len(expected)) + + for limit in rate_limits: + self.assertTrue(limit in expected) diff --git a/tests/v1/test_quota_classes.py b/tests/v1/test_quota_classes.py new file mode 100644 index 000000000..4d4cb585b --- /dev/null +++ b/tests/v1/test_quota_classes.py @@ -0,0 +1,42 @@ +# Copyright 2011 OpenStack LLC. +# 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 tests import utils +from tests.v1 import fakes + + +cs = fakes.FakeClient() + + +class QuotaClassSetsTest(utils.TestCase): + + def test_class_quotas_get(self): + class_name = 'test' + cs.quota_classes.get(class_name) + cs.assert_called('GET', '/os-quota-class-sets/%s' % class_name) + + def test_update_quota(self): + q = cs.quota_classes.get('test') + q.update(volumes=2) + cs.assert_called('PUT', '/os-quota-class-sets/test') + + def test_refresh_quota(self): + q = cs.quota_classes.get('test') + q2 = cs.quota_classes.get('test') + self.assertEqual(q.volumes, q2.volumes) + q2.volumes = 0 + self.assertNotEqual(q.volumes, q2.volumes) + q2.get() + self.assertEqual(q.volumes, q2.volumes) diff --git a/tests/v1/test_quotas.py b/tests/v1/test_quotas.py new file mode 100644 index 000000000..49f039b76 --- /dev/null +++ b/tests/v1/test_quotas.py @@ -0,0 +1,47 @@ +# Copyright 2011 OpenStack LLC. +# 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 tests import utils +from tests.v1 import fakes + + +cs = fakes.FakeClient() + + +class QuotaSetsTest(utils.TestCase): + + def test_tenant_quotas_get(self): + tenant_id = 'test' + cs.quotas.get(tenant_id) + cs.assert_called('GET', '/os-quota-sets/%s' % tenant_id) + + def test_tenant_quotas_defaults(self): + tenant_id = 'test' + cs.quotas.defaults(tenant_id) + cs.assert_called('GET', '/os-quota-sets/%s/defaults' % tenant_id) + + def test_update_quota(self): + q = cs.quotas.get('test') + q.update(volumes=2) + cs.assert_called('PUT', '/os-quota-sets/test') + + def test_refresh_quota(self): + q = cs.quotas.get('test') + q2 = cs.quotas.get('test') + self.assertEqual(q.volumes, q2.volumes) + q2.volumes = 0 + self.assertNotEqual(q.volumes, q2.volumes) + q2.get() + self.assertEqual(q.volumes, q2.volumes)