vmware-nsx/quantum/api/v2/base.py
Yong Sheng Gong 119df30750 Add quota per-tenant.
blueprint quantum-api-quotas

We implement it as an extension for linux bridge and ovs plugins.
We also expose the /quotas/Xx url to client to operate the quota.
We need admin role to show other tenant's quota, and to update quota data.
Any user can show its own tenant's quota. An DB table is used to save the
quota for each tenant.

To use it, we have in quantum.conf:
quota_driver = quantum.extensions._quotav2_driver.DbQuotaDriver

The default quotas for each tenant are defined in quantum.conf too.

In addition, modify extension framework to allow exposing a new resource and
its controler. The extension can check the environment, such as configuration
in global cfg.CONF to decide if it can be enabled.

Also, we can define enabled extensions for each plugin in extensions.py
New resources can be put into quota framework via quota_items in nova.conf

Change-Id: I54d6107fdb2808cdae1a40b501ed8c7f379dedee
2012-08-14 23:27:56 +08:00

561 lines
23 KiB
Python

# Copyright (c) 2012 OpenStack, LLC.
#
# 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 logging
import socket
import webob.exc
from quantum.api.v2 import attributes
from quantum.api.v2 import resource as wsgi_resource
from quantum.common import exceptions
from quantum.common import utils
from quantum.openstack.common import cfg
from quantum.openstack.common.notifier import api as notifier_api
from quantum import policy
from quantum import quota
LOG = logging.getLogger(__name__)
XML_NS_V20 = 'http://openstack.org/quantum/api/v2.0'
FAULT_MAP = {exceptions.NotFound: webob.exc.HTTPNotFound,
exceptions.InUse: webob.exc.HTTPConflict,
exceptions.ResourceExhausted: webob.exc.HTTPServiceUnavailable,
exceptions.MacAddressGenerationFailure:
webob.exc.HTTPServiceUnavailable,
exceptions.StateInvalid: webob.exc.HTTPBadRequest,
exceptions.InvalidInput: webob.exc.HTTPBadRequest,
exceptions.OverlappingAllocationPools: webob.exc.HTTPConflict,
exceptions.OutOfBoundsAllocationPool: webob.exc.HTTPBadRequest,
exceptions.InvalidAllocationPool: webob.exc.HTTPBadRequest,
exceptions.InvalidSharedSetting: webob.exc.HTTPConflict,
exceptions.HostRoutesExhausted: webob.exc.HTTPBadRequest,
exceptions.DNSNameServersExhausted: webob.exc.HTTPBadRequest,
}
QUOTAS = quota.QUOTAS
def _get_hostname():
return socket.gethostname()
# Register the configuration options
cfg.CONF.register_opt(cfg.StrOpt('host', default=_get_hostname()))
def _fields(request):
"""
Extracts the list of fields to return
"""
return [v for v in request.GET.getall('fields') if v]
def _filters(request, attr_info):
"""
Extracts the filters from the request string
Returns a dict of lists for the filters:
check=a&check=b&name=Bob&verbose=True&verbose=other
becomes
{'check': [u'a', u'b'], 'name': [u'Bob']}
"""
res = {}
for key in set(request.GET):
if key in ('verbose', 'fields'):
continue
values = [v for v in request.GET.getall(key) if v]
if not attr_info.get(key) and values:
res[key] = values
continue
result_values = []
convert_to = (attr_info.get(key) and attr_info[key].get('convert_to')
or None)
for value in values:
if convert_to:
try:
result_values.append(convert_to(value))
except exceptions.InvalidInput as e:
raise webob.exc.HTTPUnprocessableEntity(str(e))
else:
result_values.append(value)
if result_values:
res[key] = result_values
return res
def _verbose(request):
"""
Determines the verbose fields for a request
Returns a list of items that are requested to be verbose:
check=a&check=b&name=Bob&verbose=True&verbose=other
returns
[True]
and
check=a&check=b&name=Bob&verbose=other
returns
['other']
"""
verbose = [utils.boolize(v) for v in request.GET.getall('verbose') if v]
# NOTE(jkoelker) verbose=<bool> trumps all other verbose settings
if True in verbose:
return True
elif False in verbose:
return False
return verbose
class Controller(object):
def __init__(self, plugin, collection, resource,
attr_info, allow_bulk=False):
self._plugin = plugin
self._collection = collection
self._resource = resource
self._attr_info = attr_info
self._allow_bulk = allow_bulk
self._native_bulk = self._is_native_bulk_supported()
self._policy_attrs = [name for (name, info) in self._attr_info.items()
if info.get('required_by_policy')]
self._publisher_id = notifier_api.publisher_id('network')
def _is_native_bulk_supported(self):
native_bulk_attr_name = ("_%s__native_bulk_support"
% self._plugin.__class__.__name__)
return getattr(self._plugin, native_bulk_attr_name, False)
def _is_visible(self, attr):
attr_val = self._attr_info.get(attr)
return attr_val and attr_val['is_visible']
def _view(self, data, fields_to_strip=None):
# make sure fields_to_strip is iterable
if not fields_to_strip:
fields_to_strip = []
return dict(item for item in data.iteritems()
if self._is_visible(item[0])
and not item[0] in fields_to_strip)
def _do_field_list(self, original_fields):
fields_to_add = None
# don't do anything if fields were not specified in the request
if original_fields:
fields_to_add = [attr for attr in self._policy_attrs
if attr not in original_fields]
original_fields.extend(self._policy_attrs)
return original_fields, fields_to_add
def _items(self, request, do_authz=False):
"""Retrieves and formats a list of elements of the requested entity"""
# NOTE(salvatore-orlando): The following ensures that fields which
# are needed for authZ policy validation are not stripped away by the
# plugin before returning.
original_fields, fields_to_add = self._do_field_list(_fields(request))
kwargs = {'filters': _filters(request, self._attr_info),
'verbose': _verbose(request),
'fields': original_fields}
obj_getter = getattr(self._plugin, "get_%s" % self._collection)
obj_list = obj_getter(request.context, **kwargs)
# Check authz
if do_authz:
# FIXME(salvatore-orlando): obj_getter might return references to
# other resources. Must check authZ on them too.
# Omit items from list that should not be visible
obj_list = [obj for obj in obj_list
if policy.check(request.context,
"get_%s" % self._resource,
obj,
plugin=self._plugin)]
return {self._collection: [self._view(obj,
fields_to_strip=fields_to_add)
for obj in obj_list]}
def _item(self, request, id, do_authz=False, field_list=None):
"""Retrieves and formats a single element of the requested entity"""
kwargs = {'verbose': _verbose(request),
'fields': field_list}
action = "get_%s" % self._resource
obj_getter = getattr(self._plugin, action)
obj = obj_getter(request.context, id, **kwargs)
# Check authz
# FIXME(salvatore-orlando): obj_getter might return references to
# other resources. Must check authZ on them too.
if do_authz:
policy.enforce(request.context, action, obj, plugin=self._plugin)
return obj
def index(self, request):
"""Returns a list of the requested entity"""
return self._items(request, True)
def show(self, request, id):
"""Returns detailed information about the requested entity"""
try:
# NOTE(salvatore-orlando): The following ensures that fields
# which are needed for authZ policy validation are not stripped
# away by the plugin before returning.
field_list, added_fields = self._do_field_list(_fields(request))
return {self._resource:
self._view(self._item(request,
id,
do_authz=True,
field_list=field_list),
fields_to_strip=added_fields)}
except exceptions.PolicyNotAuthorized:
# To avoid giving away information, pretend that it
# doesn't exist
raise webob.exc.HTTPNotFound()
def _emulate_bulk_create(self, obj_creator, request, body):
objs = []
try:
for item in body[self._collection]:
kwargs = {self._resource: item}
objs.append(self._view(obj_creator(request.context,
**kwargs)))
return objs
# Note(salvatore-orlando): broad catch as in theory a plugin
# could raise any kind of exception
except Exception as ex:
for obj in objs:
delete_action = "delete_%s" % self._resource
obj_deleter = getattr(self._plugin, delete_action)
try:
obj_deleter(request.context, obj['id'])
except Exception:
# broad catch as our only purpose is to log the exception
LOG.exception("Unable to undo add for %s %s",
self._resource, obj['id'])
# TODO(salvatore-orlando): The object being processed when the
# plugin raised might have been created or not in the db.
# We need a way for ensuring that if it has been created,
# it is then deleted
raise
def create(self, request, body=None):
"""Creates a new instance of the requested entity"""
notifier_api.notify(request.context,
self._publisher_id,
self._resource + '.create.start',
notifier_api.INFO,
body)
body = Controller.prepare_request_body(request.context, body, True,
self._resource, self._attr_info,
allow_bulk=self._allow_bulk)
action = "create_%s" % self._resource
# Check authz
try:
if self._collection in body:
# Have to account for bulk create
for item in body[self._collection]:
self._validate_network_tenant_ownership(
request,
item[self._resource],
)
policy.enforce(request.context,
action,
item[self._resource],
plugin=self._plugin)
try:
count = QUOTAS.count(request.context, self._resource,
self._plugin, self._collection,
item[self._resource]['tenant_id'])
kwargs = {self._resource: count + 1}
except exceptions.QuotaResourceUnknown as e:
# We don't want to quota this resource
LOG.debug(e)
except Exception:
raise
else:
QUOTAS.limit_check(request.context,
item[self._resource]['tenant_id'],
**kwargs)
else:
self._validate_network_tenant_ownership(
request,
body[self._resource]
)
policy.enforce(request.context,
action,
body[self._resource],
plugin=self._plugin)
try:
count = QUOTAS.count(request.context, self._resource,
self._plugin, self._collection,
body[self._resource]['tenant_id'])
kwargs = {self._resource: count + 1}
except exceptions.QuotaResourceUnknown as e:
# We don't want to quota this resource
LOG.debug(e)
except Exception:
raise
else:
QUOTAS.limit_check(request.context,
body[self._resource]['tenant_id'],
**kwargs)
except exceptions.PolicyNotAuthorized:
LOG.exception("Create operation not authorized")
raise webob.exc.HTTPForbidden()
def notify(create_result):
notifier_api.notify(request.context,
self._publisher_id,
self._resource + '.create.end',
notifier_api.INFO,
create_result)
return create_result
if self._collection in body and self._native_bulk:
# plugin does atomic bulk create operations
obj_creator = getattr(self._plugin, "%s_bulk" % action)
objs = obj_creator(request.context, body)
return notify({self._collection: [self._view(obj)
for obj in objs]})
else:
obj_creator = getattr(self._plugin, action)
if self._collection in body:
# Emulate atomic bulk behavior
objs = self._emulate_bulk_create(obj_creator, request, body)
return notify({self._collection: objs})
else:
kwargs = {self._resource: body}
obj = obj_creator(request.context, **kwargs)
return notify({self._resource: self._view(obj)})
def delete(self, request, id):
"""Deletes the specified entity"""
notifier_api.notify(request.context,
self._publisher_id,
self._resource + '.delete.start',
notifier_api.INFO,
{self._resource + '_id': id})
action = "delete_%s" % self._resource
# Check authz
obj = self._item(request, id)
try:
policy.enforce(request.context,
action,
obj,
plugin=self._plugin)
except exceptions.PolicyNotAuthorized:
# To avoid giving away information, pretend that it
# doesn't exist
raise webob.exc.HTTPNotFound()
obj_deleter = getattr(self._plugin, action)
obj_deleter(request.context, id)
notifier_api.notify(request.context,
self._publisher_id,
self._resource + '.delete.end',
notifier_api.INFO,
{self._resource + '_id': id})
def update(self, request, id, body=None):
"""Updates the specified entity's attributes"""
payload = body.copy()
payload['id'] = id
notifier_api.notify(request.context,
self._publisher_id,
self._resource + '.update.start',
notifier_api.INFO,
payload)
body = Controller.prepare_request_body(request.context, body, False,
self._resource, self._attr_info,
allow_bulk=self._allow_bulk)
action = "update_%s" % self._resource
# Load object to check authz
# but pass only attributes in the original body and required
# by the policy engine to the policy 'brain'
field_list = [name for (name, value) in self._attr_info.iteritems()
if ('required_by_policy' in value and
value['required_by_policy'] or
not 'default' in value)]
orig_obj = self._item(request, id, field_list=field_list)
orig_obj.update(body)
try:
policy.enforce(request.context,
action,
orig_obj,
plugin=self._plugin)
except exceptions.PolicyNotAuthorized:
# To avoid giving away information, pretend that it
# doesn't exist
raise webob.exc.HTTPNotFound()
obj_updater = getattr(self._plugin, action)
kwargs = {self._resource: body}
obj = obj_updater(request.context, id, **kwargs)
result = {self._resource: self._view(obj)}
notifier_api.notify(request.context,
self._publisher_id,
self._resource + '.update.end',
notifier_api.INFO,
result)
return result
@staticmethod
def _populate_tenant_id(context, res_dict, is_create):
if (('tenant_id' in res_dict and
res_dict['tenant_id'] != context.tenant_id and
not context.is_admin)):
msg = _("Specifying 'tenant_id' other than authenticated "
"tenant in request requires admin privileges")
raise webob.exc.HTTPBadRequest(msg)
if is_create and 'tenant_id' not in res_dict:
if context.tenant_id:
res_dict['tenant_id'] = context.tenant_id
else:
msg = _("Running without keystyone AuthN requires "
" that tenant_id is specified")
raise webob.exc.HTTPBadRequest(msg)
@staticmethod
def prepare_request_body(context, body, is_create, resource, attr_info,
allow_bulk=False):
""" verifies required attributes are in request body, and that
an attribute is only specified if it is allowed for the given
operation (create/update).
Attribute with default values are considered to be
optional.
body argument must be the deserialized body
"""
collection = resource + "s"
if not body:
raise webob.exc.HTTPBadRequest(_("Resource body required"))
body = body or {resource: {}}
if collection in body and allow_bulk:
bulk_body = [Controller.prepare_request_body(
context, {resource: b}, is_create, resource, attr_info,
allow_bulk) if resource not in b
else Controller.prepare_request_body(
context, b, is_create, resource, attr_info, allow_bulk)
for b in body[collection]]
if not bulk_body:
raise webob.exc.HTTPBadRequest(_("Resources required"))
return {collection: bulk_body}
elif collection in body and not allow_bulk:
raise webob.exc.HTTPBadRequest("Bulk operation not supported")
res_dict = body.get(resource)
if res_dict is None:
msg = _("Unable to find '%s' in request body") % resource
raise webob.exc.HTTPBadRequest(msg)
Controller._populate_tenant_id(context, res_dict, is_create)
if is_create: # POST
for attr, attr_vals in attr_info.iteritems():
is_required = ('default' not in attr_vals and
attr_vals['allow_post'])
if is_required and attr not in res_dict:
msg = _("Failed to parse request. Required "
" attribute '%s' not specified") % attr
raise webob.exc.HTTPUnprocessableEntity(msg)
if not attr_vals['allow_post'] and attr in res_dict:
msg = _("Attribute '%s' not allowed in POST" % attr)
raise webob.exc.HTTPUnprocessableEntity(msg)
if attr_vals['allow_post']:
res_dict[attr] = res_dict.get(attr,
attr_vals.get('default'))
else: # PUT
for attr, attr_vals in attr_info.iteritems():
if attr in res_dict and not attr_vals['allow_put']:
msg = _("Cannot update read-only attribute %s") % attr
raise webob.exc.HTTPUnprocessableEntity(msg)
for attr, attr_vals in attr_info.iteritems():
# Convert values if necessary
if ('convert_to' in attr_vals and
attr in res_dict and
res_dict[attr] != attributes.ATTR_NOT_SPECIFIED):
res_dict[attr] = attr_vals['convert_to'](res_dict[attr])
# Check that configured values are correct
if not ('validate' in attr_vals and
attr in res_dict and
res_dict[attr] != attributes.ATTR_NOT_SPECIFIED):
continue
for rule in attr_vals['validate']:
res = attributes.validators[rule](res_dict[attr],
attr_vals['validate'][rule])
if res:
msg_dict = dict(attr=attr, reason=res)
msg = _("Invalid input for %(attr)s. "
"Reason: %(reason)s.") % msg_dict
raise webob.exc.HTTPUnprocessableEntity(msg)
return body
def _validate_network_tenant_ownership(self, request, resource_item):
# TODO(salvatore-orlando): consider whether this check can be folded
# in the policy engine
if self._resource not in ('port', 'subnet'):
return
network = self._plugin.get_network(
request.context,
resource_item['network_id'])
# do not perform the check on shared networks
if network.get('shared'):
return
network_owner = network['tenant_id']
if network_owner != resource_item['tenant_id']:
msg = _("Tenant %(tenant_id)s not allowed to "
"create %(resource)s on this network")
raise webob.exc.HTTPForbidden(msg % {
"tenant_id": resource_item['tenant_id'],
"resource": self._resource,
})
def create_resource(collection, resource, plugin, params, allow_bulk=False):
controller = Controller(plugin, collection, resource, params, allow_bulk)
# NOTE(jkoelker) To anyone wishing to add "proper" xml support
# this is where you do it
serializers = {}
# 'application/xml': wsgi.XMLDictSerializer(metadata, XML_NS_V20),
deserializers = {}
# 'application/xml': wsgi.XMLDeserializer(metadata),
return wsgi_resource.Resource(controller, FAULT_MAP, deserializers,
serializers)