Implement volume quota support in the cinderclient

* fix for bug 1023311
  * Implements quota extensions in cinderclient
  * Implements absolute limits and rate limits

Change-Id: I7e3f8474476cbc03efb2fefcb8400f5fec85ddcb
This commit is contained in:
John Griffith 2012-08-16 15:48:55 -06:00
parent a153f10b2b
commit f270f22cb0
9 changed files with 433 additions and 2 deletions

View File

@ -80,12 +80,14 @@ You'll find complete documentation on the shell by running
Positional arguments: Positional arguments:
<subcommand> <subcommand>
absolute-limits Print a list of absolute limits for a user
create Add a new volume. create Add a new volume.
credentials Show user credentials returned from auth credentials Show user credentials returned from auth
delete Remove a volume. delete Remove a volume.
endpoints Discover endpoints that get returned from the endpoints Discover endpoints that get returned from the
authenticate services authenticate services
list List all the volumes. list List all the volumes.
rate-limits Print a list of rate limits for a user
show Show details about a volume. show Show details about a volume.
snapshot-create Add a new snapshot. snapshot-create Add a new snapshot.
snapshot-delete Remove a snapshot. snapshot-delete Remove a snapshot.

View File

@ -1,4 +1,7 @@
from cinderclient import client 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 volumes
from cinderclient.v1 import volume_snapshots from cinderclient.v1 import volume_snapshots
from cinderclient.v1 import volume_types from cinderclient.v1 import volume_types
@ -6,7 +9,7 @@ from cinderclient.v1 import volume_types
class Client(object): 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:: Create an instance with your creds::
@ -26,16 +29,19 @@ class Client(object):
insecure=False, timeout=None, proxy_tenant_id=None, insecure=False, timeout=None, proxy_tenant_id=None,
proxy_token=None, region_name=None, proxy_token=None, region_name=None,
endpoint_type='publicURL', extensions=None, endpoint_type='publicURL', extensions=None,
service_type='compute', service_name=None, service_type='volume', service_name=None,
volume_service_name=None): volume_service_name=None):
# FIXME(comstud): Rename the api_key argument above when we # FIXME(comstud): Rename the api_key argument above when we
# know it's not being used as keyword argument # know it's not being used as keyword argument
password = api_key password = api_key
self.limits = limits.LimitsManager(self)
# extensions # extensions
self.volumes = volumes.VolumeManager(self) self.volumes = volumes.VolumeManager(self)
self.volume_snapshots = volume_snapshots.SnapshotManager(self) self.volume_snapshots = volume_snapshots.SnapshotManager(self)
self.volume_types = volume_types.VolumeTypeManager(self) self.volume_types = volume_types.VolumeTypeManager(self)
self.quota_classes = quota_classes.QuotaClassSetManager(self)
self.quotas = quotas.QuotaSetManager(self)
# Add in any extensions... # Add in any extensions...
if extensions: if extensions:

79
cinderclient/v1/limits.py Normal file
View File

@ -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 "<Limits>"
@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 "<RateLimit: method=%s uri=%s>" % (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 "<AbsoluteLimit: name=%s>" % (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")

View File

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

54
cinderclient/v1/quotas.py Normal file
View File

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

View File

@ -346,3 +346,100 @@ def do_credentials(cs, args):
catalog = cs.client.service_catalog.catalog catalog = cs.client.service_catalog.catalog
utils.print_dict(catalog['access']['user'], "User Credentials") utils.print_dict(catalog['access']['user'], "User Credentials")
utils.print_dict(catalog['access']['token'], "Token") 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='<tenant_id>',
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='<tenant_id>',
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='<tenant_id>',
help='UUID of tenant to set the quotas for.')
@utils.arg('--volumes',
metavar='<volumes>',
type=int, default=None,
help='New value for the "volumes" quota.')
@utils.arg('--gigabytes',
metavar='<gigabytes>',
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='<class>',
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='<class>',
help='Name of quota class to set the quotas for.')
@utils.arg('--volumes',
metavar='<volumes>',
type=int, default=None,
help='New value for the "volumes" quota.')
@utils.arg('--gigabytes',
metavar='<gigabytes>',
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)

52
tests/v1/test_limits.py Normal file
View File

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

View File

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

47
tests/v1/test_quotas.py Normal file
View File

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