Implement a Quota management API extension
Additionally, this adds the prerequisite plumbing Fixes bug #1199025 Change-Id: Ie84e77ce891cf17a32930326961d16a377850da0
This commit is contained in:
parent
496461b18a
commit
2e65d271e9
52
designate/api/v1/extensions/quotas.py
Normal file
52
designate/api/v1/extensions/quotas.py
Normal file
@ -0,0 +1,52 @@
|
||||
# Copyright 2012 Hewlett-Packard Development Company, L.P.
|
||||
#
|
||||
# Author: Kiall Mac Innes <kiall@hp.com>
|
||||
#
|
||||
# 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.
|
||||
import flask
|
||||
from designate.openstack.common import log as logging
|
||||
from designate.central import rpcapi as central_rpcapi
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
central_api = central_rpcapi.CentralAPI()
|
||||
blueprint = flask.Blueprint('quotas', __name__)
|
||||
|
||||
|
||||
@blueprint.route('/quotas/<tenant_id>', methods=['GET'])
|
||||
def get_quotas(tenant_id):
|
||||
context = flask.request.environ.get('context')
|
||||
|
||||
quotas = central_api.get_quotas(context, tenant_id)
|
||||
|
||||
return flask.jsonify(quotas)
|
||||
|
||||
|
||||
@blueprint.route('/quotas/<tenant_id>', methods=['PUT', 'POST'])
|
||||
def set_quota(tenant_id):
|
||||
context = flask.request.environ.get('context')
|
||||
values = flask.request.json
|
||||
|
||||
for resource, hard_limit in values.items():
|
||||
central_api.set_quota(context, tenant_id, resource, hard_limit)
|
||||
|
||||
quotas = central_api.get_quotas(context, tenant_id)
|
||||
return flask.jsonify(quotas)
|
||||
|
||||
|
||||
@blueprint.route('/quotas/<tenant_id>', methods=['DELETE'])
|
||||
def reset_quotas(tenant_id):
|
||||
context = flask.request.environ.get('context')
|
||||
|
||||
central_api.reset_quotas(context, tenant_id)
|
||||
|
||||
return flask.Response(status=200)
|
@ -65,8 +65,6 @@ cfg.CONF.register_opts([
|
||||
help='The backend driver to use'),
|
||||
cfg.StrOpt('storage-driver', default='sqlalchemy',
|
||||
help='The storage driver to use'),
|
||||
cfg.StrOpt('quota-driver', default='storage',
|
||||
help='The quota driver to use'),
|
||||
cfg.ListOpt('enabled-notification-handlers', default=[],
|
||||
help='Enabled Notification Handlers'),
|
||||
cfg.ListOpt('domain-name-blacklist',
|
||||
|
@ -31,6 +31,7 @@ class CentralAPI(rpc_proxy.RpcProxy):
|
||||
1.2 - Add get_tenant and get_tenants
|
||||
1.3 - Add get_absolute_limits
|
||||
2.0 - Renamed most get_resources to find_resources
|
||||
2.1 - Add quota methods
|
||||
|
||||
"""
|
||||
def __init__(self, topic=None):
|
||||
@ -43,6 +44,29 @@ class CentralAPI(rpc_proxy.RpcProxy):
|
||||
|
||||
return self.call(context, msg)
|
||||
|
||||
# Quota Methods
|
||||
def get_quotas(self, context, tenant_id):
|
||||
msg = self.make_msg('get_quotas', tenant_id=tenant_id)
|
||||
|
||||
return self.call(context, msg, version='2.1')
|
||||
|
||||
def get_quota(self, context, tenant_id, resource):
|
||||
msg = self.make_msg('get_quota', tenant_id=tenant_id,
|
||||
resource=resource)
|
||||
|
||||
return self.call(context, msg, version='2.1')
|
||||
|
||||
def set_quota(self, context, tenant_id, resource, hard_limit):
|
||||
msg = self.make_msg('set_quota', tenant_id=tenant_id,
|
||||
resource=resource, hard_limit=hard_limit)
|
||||
|
||||
return self.call(context, msg, version='2.1')
|
||||
|
||||
def reset_quotas(self, context, tenant_id):
|
||||
msg = self.make_msg('reset_quotas', tenant_id=tenant_id)
|
||||
|
||||
return self.call(context, msg, version='2.1')
|
||||
|
||||
# Server Methods
|
||||
def create_server(self, context, values):
|
||||
msg = self.make_msg('create_server', values=values)
|
||||
|
@ -43,7 +43,7 @@ def wrap_backend_call():
|
||||
|
||||
|
||||
class Service(rpc_service.Service):
|
||||
RPC_API_VERSION = '2.0'
|
||||
RPC_API_VERSION = '2.1'
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
backend_driver = cfg.CONF['service:central'].backend_driver
|
||||
@ -254,7 +254,38 @@ class Service(rpc_service.Service):
|
||||
|
||||
# Misc Methods
|
||||
def get_absolute_limits(self, context):
|
||||
return self.quota.get_tenant_quotas(context, context.tenant_id)
|
||||
# NOTE(Kiall): Currently, we only have quota based limits..
|
||||
return self.quota.get_quotas(context, context.tenant_id)
|
||||
|
||||
# Quota Methods
|
||||
def get_quotas(self, context, tenant_id):
|
||||
target = {'tenant_id': tenant_id}
|
||||
policy.check('get_quotas', context, target)
|
||||
|
||||
return self.quota.get_quotas(context, tenant_id)
|
||||
|
||||
def get_quota(self, context, tenant_id, resource):
|
||||
target = {'tenant_id': tenant_id, 'resource': resource}
|
||||
policy.check('get_quota', context, target)
|
||||
|
||||
return self.quota.get_quota(context, tenant_id, resource)
|
||||
|
||||
def set_quota(self, context, tenant_id, resource, hard_limit):
|
||||
target = {
|
||||
'tenant_id': tenant_id,
|
||||
'resource': resource,
|
||||
'hard_limit': hard_limit,
|
||||
}
|
||||
|
||||
policy.check('set_quota', context, target)
|
||||
|
||||
return self.quota.set_quota(context, tenant_id, resource, hard_limit)
|
||||
|
||||
def reset_quotas(self, context, tenant_id):
|
||||
target = {'tenant_id': tenant_id}
|
||||
policy.check('reset_quotas', context, target)
|
||||
|
||||
self.quota.reset_quotas(context, tenant_id)
|
||||
|
||||
# Server Methods
|
||||
def create_server(self, context, values):
|
||||
|
@ -20,6 +20,7 @@ from designate.quota.base import Quota
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
cfg.CONF.register_opts([
|
||||
cfg.StrOpt('quota-driver', default='storage', help='Quota driver to use'),
|
||||
cfg.IntOpt('quota-domains', default=10, help='Number of domains allowed '
|
||||
'per tenant'),
|
||||
cfg.IntOpt('quota-domain-records', default=500, help='Number of records '
|
||||
@ -28,6 +29,4 @@ cfg.CONF.register_opts([
|
||||
|
||||
|
||||
def get_quota():
|
||||
quota_driver = cfg.CONF['service:central'].quota_driver
|
||||
|
||||
return Quota.get_plugin(quota_driver, invoke_on_load=True)
|
||||
return Quota.get_plugin(cfg.CONF.quota_driver, invoke_on_load=True)
|
||||
|
@ -25,24 +25,8 @@ class Quota(Plugin):
|
||||
__plugin_ns__ = 'designate.quota'
|
||||
__plugin_type__ = 'quota'
|
||||
|
||||
def get_tenant_quotas(self, context, tenant_id):
|
||||
"""
|
||||
Get quotas for a tenant.
|
||||
|
||||
:param context: RPC Context.
|
||||
:param tenant_id: Tenant ID to fetch quotas for
|
||||
"""
|
||||
quotas = {
|
||||
'domains': cfg.CONF.quota_domains,
|
||||
'domain_records': cfg.CONF.quota_domain_records,
|
||||
}
|
||||
|
||||
quotas.update(self._get_tenant_quotas(context, tenant_id))
|
||||
|
||||
return quotas
|
||||
|
||||
def limit_check(self, context, tenant_id, **values):
|
||||
quotas = self.get_tenant_quotas(context, tenant_id)
|
||||
quotas = self.get_quotas(context, tenant_id)
|
||||
|
||||
for resource, value in values.items():
|
||||
if resource in quotas:
|
||||
@ -51,6 +35,33 @@ class Quota(Plugin):
|
||||
else:
|
||||
raise exceptions.QuotaResourceUnknown()
|
||||
|
||||
def get_quotas(self, context, tenant_id):
|
||||
quotas = self.get_default_quotas(context)
|
||||
|
||||
quotas.update(self._get_quotas(context, tenant_id))
|
||||
|
||||
return quotas
|
||||
|
||||
@abc.abstractmethod
|
||||
def _get_tenant_quotas(self, context, tenant_id):
|
||||
def _get_quotas(self, context, tenant_id):
|
||||
pass
|
||||
|
||||
def get_default_quotas(self, context):
|
||||
return {
|
||||
'domains': cfg.CONF.quota_domains,
|
||||
'domain_records': cfg.CONF.quota_domain_records,
|
||||
}
|
||||
|
||||
def get_quota(self, context, tenant_id, resource):
|
||||
quotas = self._get_quotas(context, tenant_id)
|
||||
|
||||
if resource not in quotas:
|
||||
raise exceptions.QuotaResourceUnknown()
|
||||
|
||||
return quotas[resource]
|
||||
|
||||
def set_quota(self, context, tenant_id, resource, hard_limit):
|
||||
raise NotImplementedError()
|
||||
|
||||
def reset_quotas(self, context, tenant_id):
|
||||
raise NotImplementedError()
|
||||
|
@ -19,9 +19,8 @@ from designate.quota.base import Quota
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ConfigQuota(Quota):
|
||||
def _get_tenant_quotas(self, context, tenant_id):
|
||||
# NOTE(kiall): The base class handles merging the config defaults into
|
||||
# the per-tenent values we return. Since we only care
|
||||
# about the config values, we return an empty dict.
|
||||
class NoopQuota(Quota):
|
||||
__plugin_name__ = 'noop'
|
||||
|
||||
def _get_quotas(self, context, tenant_id):
|
||||
return {}
|
@ -13,25 +13,78 @@
|
||||
# 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 designate import exceptions
|
||||
from designate.openstack.common import log as logging
|
||||
from designate.quota.base import Quota
|
||||
from designate import storage
|
||||
from designate.storage import api as sapi
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class StorageQuota(Quota):
|
||||
def __init__(self):
|
||||
__plugin_name__ = 'storage'
|
||||
|
||||
def __init__(self, storage_api=None):
|
||||
super(StorageQuota, self).__init__()
|
||||
|
||||
# Get a storage connection
|
||||
self.storage = storage.get_storage()
|
||||
if storage_api is None:
|
||||
storage_api = sapi.StorageAPI()
|
||||
|
||||
def _get_tenant_quotas(self, context, tenant_id):
|
||||
criterion = dict(
|
||||
tenant_id=tenant_id
|
||||
)
|
||||
self.storage_api = storage_api
|
||||
|
||||
quotas = self.storage.find_quotas(context, criterion)
|
||||
def _get_quotas(self, context, tenant_id):
|
||||
quotas = self.storage_api.find_quotas(context, {
|
||||
'tenant_id': tenant_id,
|
||||
})
|
||||
|
||||
return dict((q['resource'], q['hard_limit']) for q in quotas)
|
||||
|
||||
def get_quota(self, context, tenant_id, resource):
|
||||
quota = self.storage_api.find_quota(context, {
|
||||
'tenant_id': tenant_id,
|
||||
'resource': resource,
|
||||
})
|
||||
|
||||
return {resource: quota['hard_limit']}
|
||||
|
||||
def set_quota(self, context, tenant_id, resource, hard_limit):
|
||||
def create_quota():
|
||||
values = {
|
||||
'tenant_id': tenant_id,
|
||||
'resource': resource,
|
||||
'hard_limit': hard_limit,
|
||||
}
|
||||
|
||||
with self.storage_api.create_quota(context, values):
|
||||
pass # NOTE(kiall): No other systems need updating.
|
||||
|
||||
def update_quota():
|
||||
values = {'hard_limit': hard_limit}
|
||||
|
||||
with self.storage_api.update_quota(context, quota['id'], values):
|
||||
pass # NOTE(kiall): No other systems need updating.
|
||||
|
||||
if resource not in self.get_default_quotas(context).keys():
|
||||
raise exceptions.QuotaResourceUnknown("%s is not a valid quota "
|
||||
"resource", resource)
|
||||
|
||||
try:
|
||||
quota = self.storage_api.find_quota(context, {
|
||||
'tenant_id': tenant_id,
|
||||
'resource': resource,
|
||||
})
|
||||
except exceptions.NotFound:
|
||||
create_quota()
|
||||
else:
|
||||
update_quota()
|
||||
|
||||
return {resource: hard_limit}
|
||||
|
||||
def reset_quotas(self, context, tenant_id):
|
||||
quotas = self.storage_api.find_quotas(context, {
|
||||
'tenant_id': tenant_id,
|
||||
})
|
||||
|
||||
for quota in quotas:
|
||||
with self.storage_api.delete_quota(context, quota['id']):
|
||||
pass # NOTE(kiall): No other systems need updating.
|
||||
|
@ -51,7 +51,7 @@ class StorageAPI(object):
|
||||
"""
|
||||
return self.storage.get_quota(context, quota_id)
|
||||
|
||||
def find_quotas(self, context, criterion):
|
||||
def find_quotas(self, context, criterion=None):
|
||||
"""
|
||||
Find Quotas
|
||||
|
||||
|
@ -29,10 +29,10 @@ class QuotaTestCase(TestCase):
|
||||
super(QuotaTestCase, self).setUp()
|
||||
self.quota = quota.get_quota()
|
||||
|
||||
def test_get_tenant_quotas(self):
|
||||
def test_get_quotas(self):
|
||||
context = self.get_admin_context()
|
||||
|
||||
quotas = self.quota.get_tenant_quotas(context, 'DefaultQuotaTenant')
|
||||
quotas = self.quota.get_quotas(context, 'DefaultQuotaTenant')
|
||||
|
||||
self.assertIsNotNone(quotas)
|
||||
self.assertEqual(quotas, {
|
||||
|
@ -19,9 +19,9 @@ from designate.tests.test_quota import QuotaTestCase
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ConfigQuotaTest(QuotaTestCase):
|
||||
class NoopQuotaTest(QuotaTestCase):
|
||||
__test__ = True
|
||||
|
||||
def setUp(self):
|
||||
self.config(quota_driver='config', group='service:central')
|
||||
super(ConfigQuotaTest, self).setUp()
|
||||
self.config(quota_driver='noop')
|
||||
super(NoopQuotaTest, self).setUp()
|
@ -23,5 +23,68 @@ class StorageQuotaTest(QuotaTestCase):
|
||||
__test__ = True
|
||||
|
||||
def setUp(self):
|
||||
self.config(quota_driver='storage', group='service:central')
|
||||
self.config(quota_driver='storage')
|
||||
super(StorageQuotaTest, self).setUp()
|
||||
|
||||
def test_set_quota_create(self):
|
||||
quota = self.quota.set_quota(self.admin_context, 'tenant_id',
|
||||
'domains', 1500)
|
||||
|
||||
self.assertEquals(quota, {'domains': 1500})
|
||||
|
||||
# Drop into the storage layer directly to ensure the quota was created
|
||||
# sucessfully.
|
||||
criterion = {
|
||||
'tenant_id': 'tenant_id',
|
||||
'resource': 'domains'
|
||||
}
|
||||
|
||||
quota = self.quota.storage_api.find_quota(self.admin_context,
|
||||
criterion)
|
||||
|
||||
self.assertEquals(quota['tenant_id'], 'tenant_id')
|
||||
self.assertEquals(quota['resource'], 'domains')
|
||||
self.assertEquals(quota['hard_limit'], 1500)
|
||||
|
||||
def test_set_quota_update(self):
|
||||
# First up, Create the quota
|
||||
self.quota.set_quota(self.admin_context, 'tenant_id', 'domains', 1500)
|
||||
|
||||
# Next, update the quota
|
||||
self.quota.set_quota(self.admin_context, 'tenant_id', 'domains', 1234)
|
||||
|
||||
# Drop into the storage layer directly to ensure the quota was updated
|
||||
# sucessfully
|
||||
criterion = {
|
||||
'tenant_id': 'tenant_id',
|
||||
'resource': 'domains'
|
||||
}
|
||||
|
||||
quota = self.quota.storage_api.find_quota(self.admin_context,
|
||||
criterion)
|
||||
|
||||
self.assertEquals(quota['tenant_id'], 'tenant_id')
|
||||
self.assertEquals(quota['resource'], 'domains')
|
||||
self.assertEquals(quota['hard_limit'], 1234)
|
||||
|
||||
def test_reset_quotas(self):
|
||||
# First up, Create a domains quota
|
||||
self.quota.set_quota(self.admin_context, 'tenant_id', 'domains', 1500)
|
||||
|
||||
# Then, Create a domain_records quota
|
||||
self.quota.set_quota(self.admin_context, 'tenant_id', 'domain_records',
|
||||
800)
|
||||
|
||||
# Now, Reset the tenants quota
|
||||
self.quota.reset_quotas(self.admin_context, 'tenant_id')
|
||||
|
||||
# Drop into the storage layer directly to ensure the tenant has no
|
||||
# specific quotas registed.
|
||||
criterion = {
|
||||
'tenant_id': 'tenant_id'
|
||||
}
|
||||
|
||||
quotas = self.quota.storage_api.find_quotas(self.admin_context,
|
||||
criterion)
|
||||
|
||||
self.assertEquals(0, len(quotas))
|
||||
|
@ -62,7 +62,7 @@ default_log_levels = amqplib=WARN, sqlalchemy=WARN, boto=WARN, suds=INFO, keysto
|
||||
#auth_strategy = noauth
|
||||
|
||||
# Enabled API Version 1 extensions
|
||||
#enabled_extensions_v1 = diagnostics, sync, import, export, reports
|
||||
#enabled_extensions_v1 = diagnostics, quotas, reports, sync
|
||||
|
||||
#-----------------------
|
||||
# Agent Service
|
||||
|
@ -5,6 +5,11 @@
|
||||
|
||||
"default": "rule:admin_or_owner",
|
||||
|
||||
"get_quotas": "rule:admin_or_owner",
|
||||
"get_quota": "rule:admin_or_owner",
|
||||
"set_quota": "rule:admin",
|
||||
"reset_quotas": "rule:admin",
|
||||
|
||||
"create_server": "rule:admin",
|
||||
"find_servers": "rule:admin",
|
||||
"get_server": "rule:admin",
|
||||
|
@ -52,6 +52,7 @@ designate.api.v1 =
|
||||
|
||||
designate.api.v1.extensions =
|
||||
diagnostics = designate.api.v1.extensions.diagnostics:blueprint
|
||||
quotas = designate.api.v1.extensions.quotas:blueprint
|
||||
sync = designate.api.v1.extensions.sync:blueprint
|
||||
reports = designate.api.v1.extensions.reports:blueprint
|
||||
|
||||
@ -71,8 +72,8 @@ designate.backend =
|
||||
fake = designate.backend.impl_fake:FakeBackend
|
||||
|
||||
designate.quota =
|
||||
noop = designate.quota.impl_noop:NoopQuota
|
||||
storage = designate.quota.impl_storage:StorageQuota
|
||||
config = designate.quota.impl_config:ConfigQuota
|
||||
|
||||
designate.manage =
|
||||
database-init = designate.manage.database:InitCommand
|
||||
|
Loading…
Reference in New Issue
Block a user