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:
parent
a153f10b2b
commit
f270f22cb0
@ -80,12 +80,14 @@ You'll find complete documentation on the shell by running
|
||||
|
||||
Positional arguments:
|
||||
<subcommand>
|
||||
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.
|
||||
|
@ -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:
|
||||
|
79
cinderclient/v1/limits.py
Normal file
79
cinderclient/v1/limits.py
Normal 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")
|
52
cinderclient/v1/quota_classes.py
Normal file
52
cinderclient/v1/quota_classes.py
Normal 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
54
cinderclient/v1/quotas.py
Normal 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')
|
@ -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='<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
52
tests/v1/test_limits.py
Normal 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)
|
42
tests/v1/test_quota_classes.py
Normal file
42
tests/v1/test_quota_classes.py
Normal 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
47
tests/v1/test_quotas.py
Normal 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)
|
Loading…
x
Reference in New Issue
Block a user