Adds absolute limits to limit API call

Addresses XML issues

Change-Id: I96df93c36c06baf309f881fc1f21b5acbd7fa953
Fixes: bug #1154298
This commit is contained in:
daniel-a-nguyen 2013-03-12 20:52:04 -07:00
parent 2187352dad
commit c27fd2447a
10 changed files with 384 additions and 1270 deletions

3
.gitignore vendored
View File

@ -21,3 +21,6 @@ Changelog
reddwarf.iml
atlassian-ide-plugin.xml
.testrepository
.pid
.project
.pydevproject

View File

@ -7,7 +7,7 @@ use = call:reddwarf.common.wsgi:versioned_urlmap
paste.app_factory = reddwarf.versions:app_factory
[pipeline:reddwarfapi]
pipeline = faultwrapper tokenauth authorization contextwrapper extensions reddwarfapp
pipeline = faultwrapper tokenauth authorization contextwrapper extensions ratelimit reddwarfapp
#pipeline = debug extensions reddwarfapp
[filter:extensions]
@ -34,6 +34,9 @@ paste.filter_factory = reddwarf.common.wsgi:ContextMiddleware.factory
[filter:faultwrapper]
paste.filter_factory = reddwarf.common.wsgi:FaultWrapper.factory
[filter:ratelimit]
paste.filter_factory = reddwarf.common.limits:RateLimitingMiddleware.factory
[app:reddwarfapp]
paste.app_factory = reddwarf.common.api:app_factory

View File

@ -69,6 +69,12 @@ max_instances_per_user = 55
max_volumes_per_user = 100
volume_time_out=30
# Config options for rate limits
http_get_rate = 200
http_post_rate = 200
http_put_rate = 200
http_delete_rate = 200
# Auth
admin_roles = admin

View File

@ -45,24 +45,6 @@
"is_admin":false,
"services": ["reddwarf"]
}
},
{
"auth_user":"rate_limit",
"auth_key":"password",
"tenant":"4000",
"requirements": {
"is_admin":false,
"services": ["reddwarf"]
}
},
{
"auth_user":"rate_limit_exceeded",
"auth_key":"password",
"tenant":"4050",
"requirements": {
"is_admin":false,
"services": ["reddwarf"]
}
}
],

View File

@ -25,7 +25,6 @@ import re
import time
import webob.dec
import webob.exc
import xmlutil
from reddwarf.common import cfg
from reddwarf.common import wsgi as base_wsgi
@ -34,13 +33,6 @@ from reddwarf.openstack.common import jsonutils
from reddwarf.openstack.common import wsgi
from reddwarf.openstack.common.gettextutils import _
#
# TODO: come back to this later
# Dan Nguyen
#
#from nova import quota
#QUOTAS = quota.QUOTAS
CONF = cfg.CONF
@ -51,34 +43,6 @@ PER_HOUR = 60 * 60
PER_DAY = 60 * 60 * 24
limits_nsmap = {None: xmlutil.XMLNS_COMMON_V10, 'atom': xmlutil.XMLNS_ATOM}
class LimitsTemplate(xmlutil.TemplateBuilder):
def construct(self):
root = xmlutil.TemplateElement('limits', selector='limits')
rates = xmlutil.SubTemplateElement(root, 'rates')
rate = xmlutil.SubTemplateElement(rates, 'rate', selector='rate')
rate.set('uri', 'uri')
rate.set('regex', 'regex')
limit = xmlutil.SubTemplateElement(rate, 'limit', selector='limit')
limit.set('value', 'value')
limit.set('verb', 'verb')
limit.set('remaining', 'remaining')
limit.set('unit', 'unit')
limit.set('next-available', 'next-available')
absolute = xmlutil.SubTemplateElement(root, 'absolute',
selector='absolute')
limit = xmlutil.SubTemplateElement(absolute, 'limit',
selector=xmlutil.get_items)
limit.set('name', 0)
limit.set('value', 1)
return xmlutil.MasterTemplate(root, 1, nsmap=limits_nsmap)
class Limit(object):
"""
Stores information about a limit for HTTP requests.

View File

@ -1,910 +0,0 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# 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.
import os.path
from lxml import etree
XMLNS_V10 = 'http://docs.rackspacecloud.com/servers/api/v1.0'
XMLNS_V11 = 'http://docs.openstack.org/database/api/v1.1'
XMLNS_COMMON_V10 = 'http://docs.openstack.org/common/api/v1.0'
XMLNS_ATOM = 'http://www.w3.org/2005/Atom'
def validate_schema(xml, schema_name):
if isinstance(xml, str):
xml = etree.fromstring(xml)
base_path = 'reddwarf/common/schemas/v1.1/'
if schema_name in ('atom', 'atom-link'):
base_path = 'reddwarf/common/schemas/'
# TODO: need to figure out our schema paths later
import reddwarf
schema_path = os.path.join(os.path.abspath(reddwarf.__file__)
.split('reddwarf/__init__.py')[0],
'%s%s.rng' % (base_path, schema_name))
schema_doc = etree.parse(schema_path)
relaxng = etree.RelaxNG(schema_doc)
relaxng.assertValid(xml)
class Selector(object):
"""Selects datum to operate on from an object."""
def __init__(self, *chain):
"""Initialize the selector.
Each argument is a subsequent index into the object.
"""
self.chain = chain
def __repr__(self):
"""Return a representation of the selector."""
return "Selector" + repr(self.chain)
def __call__(self, obj, do_raise=False):
"""Select a datum to operate on.
Selects the relevant datum within the object.
:param obj: The object from which to select the object.
:param do_raise: If False (the default), return None if the
indexed datum does not exist. Otherwise,
raise a KeyError.
"""
# Walk the selector list
for elem in self.chain:
# If it's callable, call it
if callable(elem):
obj = elem(obj)
else:
# Use indexing
try:
obj = obj[elem]
except (KeyError, IndexError):
# No sense going any further
if do_raise:
# Convert to a KeyError, for consistency
raise KeyError(elem)
return None
# Return the finally-selected object
return obj
def get_items(obj):
"""Get items in obj."""
return list(obj.items())
class EmptyStringSelector(Selector):
"""Returns the empty string if Selector would return None."""
def __call__(self, obj, do_raise=False):
"""Returns empty string if the selected value does not exist."""
try:
return super(EmptyStringSelector, self).__call__(obj, True)
except KeyError:
return ""
class ConstantSelector(object):
"""Returns a constant."""
def __init__(self, value):
"""Initialize the selector.
:param value: The value to return.
"""
self.value = value
def __repr__(self):
"""Return a representation of the selector."""
return repr(self.value)
def __call__(self, _obj, _do_raise=False):
"""Select a datum to operate on.
Returns a constant value. Compatible with
Selector.__call__().
"""
return self.value
class TemplateElement(object):
"""Represent an element in the template."""
def __init__(self, tag, attrib=None, selector=None, subselector=None,
**extra):
"""Initialize an element.
Initializes an element in the template. Keyword arguments
specify attributes to be set on the element; values must be
callables. See TemplateElement.set() for more information.
:param tag: The name of the tag to create.
:param attrib: An optional dictionary of element attributes.
:param selector: An optional callable taking an object and
optional boolean do_raise indicator and
returning the object bound to the element.
:param subselector: An optional callable taking an object and
optional boolean do_raise indicator and
returning the object bound to the element.
This is used to further refine the datum
object returned by selector in the event
that it is a list of objects.
"""
# Convert selector into a Selector
if selector is None:
selector = Selector()
elif not callable(selector):
selector = Selector(selector)
# Convert subselector into a Selector
if subselector is not None and not callable(subselector):
subselector = Selector(subselector)
self.tag = tag
self.selector = selector
self.subselector = subselector
self.attrib = {}
self._text = None
self._children = []
self._childmap = {}
# Run the incoming attributes through set() so that they
# become selectorized
if not attrib:
attrib = {}
attrib.update(extra)
for k, v in attrib.items():
self.set(k, v)
def __repr__(self):
"""Return a representation of the template element."""
return ('<%s.%s %r at %#x>' %
(self.__class__.__module__, self.__class__.__name__,
self.tag, id(self)))
def __len__(self):
"""Return the number of child elements."""
return len(self._children)
def __contains__(self, key):
"""Determine whether a child node named by key exists."""
return key in self._childmap
def __getitem__(self, idx):
"""Retrieve a child node by index or name."""
if isinstance(idx, basestring):
# Allow access by node name
return self._childmap[idx]
else:
return self._children[idx]
def append(self, elem):
"""Append a child to the element."""
# Unwrap templates...
elem = elem.unwrap()
# Avoid duplications
if elem.tag in self._childmap:
raise KeyError(elem.tag)
self._children.append(elem)
self._childmap[elem.tag] = elem
def extend(self, elems):
"""Append children to the element."""
# Pre-evaluate the elements
elemmap = {}
elemlist = []
for elem in elems:
# Unwrap templates...
elem = elem.unwrap()
# Avoid duplications
if elem.tag in self._childmap or elem.tag in elemmap:
raise KeyError(elem.tag)
elemmap[elem.tag] = elem
elemlist.append(elem)
# Update the children
self._children.extend(elemlist)
self._childmap.update(elemmap)
def insert(self, idx, elem):
"""Insert a child element at the given index."""
# Unwrap templates...
elem = elem.unwrap()
# Avoid duplications
if elem.tag in self._childmap:
raise KeyError(elem.tag)
self._children.insert(idx, elem)
self._childmap[elem.tag] = elem
def remove(self, elem):
"""Remove a child element."""
# Unwrap templates...
elem = elem.unwrap()
# Check if element exists
if elem.tag not in self._childmap or self._childmap[elem.tag] != elem:
raise ValueError(_('element is not a child'))
self._children.remove(elem)
del self._childmap[elem.tag]
def get(self, key):
"""Get an attribute.
Returns a callable which performs datum selection.
:param key: The name of the attribute to get.
"""
return self.attrib[key]
def set(self, key, value=None):
"""Set an attribute.
:param key: The name of the attribute to set.
:param value: A callable taking an object and optional boolean
do_raise indicator and returning the datum bound
to the attribute. If None, a Selector() will be
constructed from the key. If a string, a
Selector() will be constructed from the string.
"""
# Convert value to a selector
if value is None:
value = Selector(key)
elif not callable(value):
value = Selector(value)
self.attrib[key] = value
def keys(self):
"""Return the attribute names."""
return self.attrib.keys()
def items(self):
"""Return the attribute names and values."""
return self.attrib.items()
def unwrap(self):
"""Unwraps a template to return a template element."""
# We are a template element
return self
def wrap(self):
"""Wraps a template element to return a template."""
# Wrap in a basic Template
return Template(self)
def apply(self, elem, obj):
"""Apply text and attributes to an etree.Element.
Applies the text and attribute instructions in the template
element to an etree.Element instance.
:param elem: An etree.Element instance.
:param obj: The base object associated with this template
element.
"""
# Start with the text...
if self.text is not None:
elem.text = unicode(self.text(obj))
# Now set up all the attributes...
for key, value in self.attrib.items():
try:
elem.set(key, unicode(value(obj, True)))
except KeyError:
# Attribute has no value, so don't include it
pass
def _render(self, parent, datum, patches, nsmap):
"""Internal rendering.
Renders the template node into an etree.Element object.
Returns the etree.Element object.
:param parent: The parent etree.Element instance.
:param datum: The datum associated with this template element.
:param patches: A list of other template elements that must
also be applied.
:param nsmap: An optional namespace dictionary to be
associated with the etree.Element instance.
"""
# Allocate a node
if callable(self.tag):
tagname = self.tag(datum)
else:
tagname = self.tag
elem = etree.Element(tagname, nsmap=nsmap)
# If we have a parent, append the node to the parent
if parent is not None:
parent.append(elem)
# If the datum is None, do nothing else
if datum is None:
return elem
# Apply this template element to the element
self.apply(elem, datum)
# Additionally, apply the patches
for patch in patches:
patch.apply(elem, datum)
# We have fully rendered the element; return it
return elem
def render(self, parent, obj, patches=[], nsmap=None):
"""Render an object.
Renders an object against this template node. Returns a list
of two-item tuples, where the first item is an etree.Element
instance and the second item is the datum associated with that
instance.
:param parent: The parent for the etree.Element instances.
:param obj: The object to render this template element
against.
:param patches: A list of other template elements to apply
when rendering this template element.
:param nsmap: An optional namespace dictionary to attach to
the etree.Element instances.
"""
# First, get the datum we're rendering
data = None if obj is None else self.selector(obj)
# Check if we should render at all
if not self.will_render(data):
return []
elif data is None:
return [(self._render(parent, None, patches, nsmap), None)]
# Make the data into a list if it isn't already
if not isinstance(data, list):
data = [data]
elif parent is None:
raise ValueError(_('root element selecting a list'))
# Render all the elements
elems = []
for datum in data:
if self.subselector is not None:
datum = self.subselector(datum)
elems.append((self._render(parent, datum, patches, nsmap), datum))
# Return all the elements rendered, as well as the
# corresponding datum for the next step down the tree
return elems
def will_render(self, datum):
"""Hook method.
An overridable hook method to determine whether this template
element will be rendered at all. By default, returns False
(inhibiting rendering) if the datum is None.
:param datum: The datum associated with this template element.
"""
# Don't render if datum is None
return datum is not None
def _text_get(self):
"""Template element text.
Either None or a callable taking an object and optional
boolean do_raise indicator and returning the datum bound to
the text of the template element.
"""
return self._text
def _text_set(self, value):
# Convert value to a selector
if value is not None and not callable(value):
value = Selector(value)
self._text = value
def _text_del(self):
self._text = None
text = property(_text_get, _text_set, _text_del)
def tree(self):
"""Return string representation of the template tree.
Returns a representation of the template rooted at this
element as a string, suitable for inclusion in debug logs.
"""
# Build the inner contents of the tag...
contents = [self.tag, '!selector=%r' % self.selector]
# Add the text...
if self.text is not None:
contents.append('!text=%r' % self.text)
# Add all the other attributes
for key, value in self.attrib.items():
contents.append('%s=%r' % (key, value))
# If there are no children, return it as a closed tag
if len(self) == 0:
return '<%s/>' % ' '.join([str(i) for i in contents])
# OK, recurse to our children
children = [c.tree() for c in self]
# Return the result
return ('<%s>%s</%s>' %
(' '.join(contents), ''.join(children), self.tag))
def SubTemplateElement(parent, tag, attrib=None, selector=None,
subselector=None, **extra):
"""Create a template element as a child of another.
Corresponds to the etree.SubElement interface. Parameters are as
for TemplateElement, with the addition of the parent.
"""
# Convert attributes
attrib = attrib or {}
attrib.update(extra)
# Get a TemplateElement
elem = TemplateElement(tag, attrib=attrib, selector=selector,
subselector=subselector)
# Append the parent safely
if parent is not None:
parent.append(elem)
return elem
class Template(object):
"""Represent a template."""
def __init__(self, root, nsmap=None):
"""Initialize a template.
:param root: The root element of the template.
:param nsmap: An optional namespace dictionary to be
associated with the root element of the
template.
"""
self.root = root.unwrap() if root is not None else None
self.nsmap = nsmap or {}
self.serialize_options = dict(encoding='UTF-8', xml_declaration=True)
def _serialize(self, parent, obj, siblings, nsmap=None):
"""Internal serialization.
Recursive routine to build a tree of etree.Element instances
from an object based on the template. Returns the first
etree.Element instance rendered, or None.
:param parent: The parent etree.Element instance. Can be
None.
:param obj: The object to render.
:param siblings: The TemplateElement instances against which
to render the object.
:param nsmap: An optional namespace dictionary to be
associated with the etree.Element instance
rendered.
"""
# First step, render the element
elems = siblings[0].render(parent, obj, siblings[1:], nsmap)
# Now, recurse to all child elements
seen = set()
for idx, sibling in enumerate(siblings):
for child in sibling:
# Have we handled this child already?
if child.tag in seen:
continue
seen.add(child.tag)
# Determine the child's siblings
nieces = [child]
for sib in siblings[idx + 1:]:
if child.tag in sib:
nieces.append(sib[child.tag])
# Now we recurse for every data element
for elem, datum in elems:
self._serialize(elem, datum, nieces)
# Return the first element; at the top level, this will be the
# root element
if elems:
return elems[0][0]
def serialize(self, obj, *args, **kwargs):
"""Serialize an object.
Serializes an object against the template. Returns a string
with the serialized XML. Positional and keyword arguments are
passed to etree.tostring().
:param obj: The object to serialize.
"""
elem = self.make_tree(obj)
if elem is None:
return ''
for k, v in self.serialize_options.items():
kwargs.setdefault(k, v)
# Serialize it into XML
return etree.tostring(elem, *args, **kwargs)
def make_tree(self, obj):
"""Create a tree.
Serializes an object against the template. Returns an Element
node with appropriate children.
:param obj: The object to serialize.
"""
# If the template is empty, return the empty string
if self.root is None:
return None
# Get the siblings and nsmap of the root element
siblings = self._siblings()
nsmap = self._nsmap()
# Form the element tree
return self._serialize(None, obj, siblings, nsmap)
def _siblings(self):
"""Hook method for computing root siblings.
An overridable hook method to return the siblings of the root
element. By default, this is the root element itself.
"""
return [self.root]
def _nsmap(self):
"""Hook method for computing the namespace dictionary.
An overridable hook method to return the namespace dictionary.
"""
return self.nsmap.copy()
def unwrap(self):
"""Unwraps a template to return a template element."""
# Return the root element
return self.root
def wrap(self):
"""Wraps a template element to return a template."""
# We are a template
return self
def apply(self, master):
"""Hook method for determining slave applicability.
An overridable hook method used to determine if this template
is applicable as a slave to a given master template.
:param master: The master template to test.
"""
return True
def tree(self):
"""Return string representation of the template tree.
Returns a representation of the template as a string, suitable
for inclusion in debug logs.
"""
return "%r: %s" % (self, self.root.tree())
class MasterTemplate(Template):
"""Represent a master template.
Master templates are versioned derivatives of templates that
additionally allow slave templates to be attached. Slave
templates allow modification of the serialized result without
directly changing the master.
"""
def __init__(self, root, version, nsmap=None):
"""Initialize a master template.
:param root: The root element of the template.
:param version: The version number of the template.
:param nsmap: An optional namespace dictionary to be
associated with the root element of the
template.
"""
super(MasterTemplate, self).__init__(root, nsmap)
self.version = version
self.slaves = []
def __repr__(self):
"""Return string representation of the template."""
return ("<%s.%s object version %s at %#x>" %
(self.__class__.__module__, self.__class__.__name__,
self.version, id(self)))
def _siblings(self):
"""Hook method for computing root siblings.
An overridable hook method to return the siblings of the root
element. This is the root element plus the root elements of
all the slave templates.
"""
return [self.root] + [slave.root for slave in self.slaves]
def _nsmap(self):
"""Hook method for computing the namespace dictionary.
An overridable hook method to return the namespace dictionary.
The namespace dictionary is computed by taking the master
template's namespace dictionary and updating it from all the
slave templates.
"""
nsmap = self.nsmap.copy()
for slave in self.slaves:
nsmap.update(slave._nsmap())
return nsmap
def attach(self, *slaves):
"""Attach one or more slave templates.
Attaches one or more slave templates to the master template.
Slave templates must have a root element with the same tag as
the master template. The slave template's apply() method will
be called to determine if the slave should be applied to this
master; if it returns False, that slave will be skipped.
(This allows filtering of slaves based on the version of the
master template.)
"""
slave_list = []
for slave in slaves:
slave = slave.wrap()
# Make sure we have a tree match
if slave.root.tag != self.root.tag:
slavetag = slave.root.tag
mastertag = self.root.tag
msg = _("Template tree mismatch; adding slave %(slavetag)s "
"to master %(mastertag)s") % locals()
raise ValueError(msg)
# Make sure slave applies to this template
if not slave.apply(self):
continue
slave_list.append(slave)
# Add the slaves
self.slaves.extend(slave_list)
def copy(self):
"""Return a copy of this master template."""
# Return a copy of the MasterTemplate
tmp = self.__class__(self.root, self.version, self.nsmap)
tmp.slaves = self.slaves[:]
return tmp
class SlaveTemplate(Template):
"""Represent a slave template.
Slave templates are versioned derivatives of templates. Each
slave has a minimum version and optional maximum version of the
master template to which they can be attached.
"""
def __init__(self, root, min_vers, max_vers=None, nsmap=None):
"""Initialize a slave template.
:param root: The root element of the template.
:param min_vers: The minimum permissible version of the master
template for this slave template to apply.
:param max_vers: An optional upper bound for the master
template version.
:param nsmap: An optional namespace dictionary to be
associated with the root element of the
template.
"""
super(SlaveTemplate, self).__init__(root, nsmap)
self.min_vers = min_vers
self.max_vers = max_vers
def __repr__(self):
"""Return string representation of the template."""
return ("<%s.%s object versions %s-%s at %#x>" %
(self.__class__.__module__, self.__class__.__name__,
self.min_vers, self.max_vers, id(self)))
def apply(self, master):
"""Hook method for determining slave applicability.
An overridable hook method used to determine if this template
is applicable as a slave to a given master template. This
version requires the master template to have a version number
between min_vers and max_vers.
:param master: The master template to test.
"""
# Does the master meet our minimum version requirement?
if master.version < self.min_vers:
return False
# How about our maximum version requirement?
if self.max_vers is not None and master.version > self.max_vers:
return False
return True
class TemplateBuilder(object):
"""Template builder.
This class exists to allow templates to be lazily built without
having to build them each time they are needed. It must be
subclassed, and the subclass must implement the construct()
method, which must return a Template (or subclass) instance. The
constructor will always return the template returned by
construct(), or, if it has a copy() method, a copy of that
template.
"""
_tmpl = None
def __new__(cls, copy=True):
"""Construct and return a template.
:param copy: If True (the default), a copy of the template
will be constructed and returned, if possible.
"""
# Do we need to construct the template?
if cls._tmpl is None:
tmp = super(TemplateBuilder, cls).__new__(cls)
# Construct the template
cls._tmpl = tmp.construct()
# If the template has a copy attribute, return the result of
# calling it
if copy and hasattr(cls._tmpl, 'copy'):
return cls._tmpl.copy()
# Return the template
return cls._tmpl
def construct(self):
"""Construct a template.
Called to construct a template instance, which it must return.
Only called once.
"""
raise NotImplementedError(_("subclasses must implement construct()!"))
def make_links(parent, selector=None):
"""
Attach an Atom <links> element to the parent.
"""
elem = SubTemplateElement(parent, '{%s}link' % XMLNS_ATOM,
selector=selector)
elem.set('rel')
elem.set('type')
elem.set('href')
# Just for completeness...
return elem
def make_flat_dict(name, selector=None, subselector=None, ns=None):
"""
Utility for simple XML templates that traditionally used
XMLDictSerializer with no metadata. Returns a template element
where the top-level element has the given tag name, and where
sub-elements have tag names derived from the object's keys and
text derived from the object's values. This only works for flat
dictionary objects, not dictionaries containing nested lists or
dictionaries.
"""
# Set up the names we need...
if ns is None:
elemname = name
tagname = Selector(0)
else:
elemname = '{%s}%s' % (ns, name)
tagname = lambda obj, do_raise=False: '{%s}%s' % (ns, obj[0])
if selector is None:
selector = name
# Build the root element
root = TemplateElement(elemname, selector=selector,
subselector=subselector)
# Build an element to represent all the keys and values
elem = SubTemplateElement(root, tagname, selector=get_items)
elem.text = 1
# Return the template
return root

View File

@ -15,35 +15,23 @@
# License for the specific language governing permissions and limitations
# under the License.
from reddwarf.common import wsgi as base_wsgi
from reddwarf.common.limits import LimitsTemplate
from reddwarf.common import wsgi
from reddwarf.limits import views
from reddwarf.openstack.common import wsgi
from reddwarf.quota.quota import QUOTAS
class LimitsController(base_wsgi.Controller):
class LimitsController(wsgi.Controller):
"""
Controller for accessing limits in the OpenStack API.
Note: this is a little different than how other controllers are implemented
"""
@base_wsgi.serializers(xml=LimitsTemplate)
def index(self, req, tenant_id):
"""
Return all global and rate limit information.
Return all absolute and rate limit information.
"""
context = req.environ[base_wsgi.CONTEXT_KEY]
#
# TODO: hook this in later
#quotas = QUOTAS.get_project_quotas(context, context.project_id,
# usages=False)
#abs_limits = dict((k, v['limit']) for k, v in quotas.items())
abs_limits = {}
quotas = QUOTAS.get_all_quotas_by_tenant(tenant_id)
abs_limits = dict((k, v['hard_limit']) for k, v in quotas.items())
rate_limits = req.environ.get("reddwarf.limits", [])
builder = self._get_view_builder(req)
return builder.build(rate_limits, abs_limits)
def _get_view_builder(self, req):
return views.ViewBuilder()
return wsgi.Result(views.LimitViews(abs_limits,
rate_limits).data(), 200)

View File

@ -1,6 +1,6 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2010-2011 OpenStack LLC.
# Copyright 2013 OpenStack LLC.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
@ -16,83 +16,44 @@
# under the License.
import datetime
from reddwarf.openstack.common import timeutils
class ViewBuilder(object):
"""OpenStack API base limits view builder."""
class LimitView(object):
def build(self, rate_limits, absolute_limits):
rate_limits = self._build_rate_limits(rate_limits)
absolute_limits = self._build_absolute_limits(absolute_limits)
def __init__(self, rate_limit):
self.rate_limit = rate_limit
output = {
"limits": {
"rate": rate_limits,
"absolute": absolute_limits,
},
def data(self):
get_utc = datetime.datetime.utcfromtimestamp
next_avail = get_utc(self.rate_limit.get("resetTime", 0))
return {"limit": {
"nextAvailable": timeutils.isotime(at=next_avail),
"remaining": self.rate_limit.get("remaining", 0),
"unit": self.rate_limit.get("unit", ""),
"value": self.rate_limit.get("value", ""),
"verb": self.rate_limit.get("verb", ""),
"uri": self.rate_limit.get("URI", ""),
"regex": self.rate_limit.get("regex", "")
}
}
return output
def _build_absolute_limits(self, absolute_limits):
"""Builder for absolute limits
class LimitViews(object):
absolute_limits should be given as a dict of limits.
For example: {"ram": 512, "gigabytes": 1024}.
def __init__(self, abs_limits, rate_limits):
self.abs_limits = abs_limits
self.rate_limits = rate_limits
"""
limit_names = {
"ram": ["maxTotalRAMSize"],
"instances": ["maxTotalInstances"],
"cores": ["maxTotalCores"],
"metadata_items": ["maxServerMeta", "maxImageMeta"],
"injected_files": ["maxPersonality"],
"injected_file_content_bytes": ["maxPersonalitySize"],
"security_groups": ["maxSecurityGroups"],
"security_group_rules": ["maxSecurityGroupRules"],
}
limits = {}
for name, value in absolute_limits.iteritems():
if name in limit_names and value is not None:
for name in limit_names[name]:
limits[name] = value
return limits
def data(self):
data = []
abs_view = dict()
abs_view["verb"] = "ABSOLUTE"
abs_view["maxTotalInstances"] = self.abs_limits.get("instances", 0)
abs_view["maxTotalVolumes"] = self.abs_limits.get("volumes", 0)
def _build_rate_limits(self, rate_limits):
limits = []
for rate_limit in rate_limits:
_rate_limit_key = None
_rate_limit = self._build_rate_limit(rate_limit)
# check for existing key
for limit in limits:
if (limit["uri"] == rate_limit["URI"] and
limit["regex"] == rate_limit["regex"]):
_rate_limit_key = limit
break
# ensure we have a key if we didn't find one
if not _rate_limit_key:
_rate_limit_key = {
"uri": rate_limit["URI"],
"regex": rate_limit["regex"],
"limit": [],
}
limits.append(_rate_limit_key)
_rate_limit_key["limit"].append(_rate_limit)
return limits
def _build_rate_limit(self, rate_limit):
_get_utc = datetime.datetime.utcfromtimestamp
next_avail = _get_utc(rate_limit["resetTime"])
return {
"verb": rate_limit["verb"],
"value": rate_limit["value"],
"remaining": int(rate_limit["remaining"]),
"unit": rate_limit["unit"],
"next-available": timeutils.isotime(at=next_avail),
}
data.append(abs_view)
for l in self.rate_limits:
data.append(LimitView(l).data()["limit"])
return {"limits": data}

View File

@ -7,15 +7,14 @@ from proboscis import test
from reddwarf.openstack.common import timeutils
from reddwarf.tests.util import create_dbaas_client
from reddwarf.tests.util import test_config
from reddwarfclient import exceptions
from datetime import datetime
from reddwarf.tests.util.users import Users
GROUP = "dbaas.api.limits"
DEFAULT_RATE = 200
# Note: This should not be enabled until rd-client merges
RD_CLIENT_OK = False
DEFAULT_MAX_VOLUMES = 100
DEFAULT_MAX_INSTANCES = 55
@test(groups=[GROUP])
@ -23,69 +22,85 @@ class Limits(object):
@before_class
def setUp(self):
users = [
{
"auth_user": "rate_limit",
"auth_key": "password",
"tenant": "4000",
"requirements": {
"is_admin": False,
"services": ["reddwarf"]
}
},
{
"auth_user": "rate_limit_exceeded",
"auth_key": "password",
"tenant": "4050",
"requirements": {
"is_admin": False,
"services": ["reddwarf"]
}
}]
self._users = Users(users)
rate_user = self._get_user('rate_limit')
self.rd_client = create_dbaas_client(rate_user)
def _get_user(self, name):
return test_config.users.find_user_by_name(name)
def _get_next_available(self, resource):
return resource.__dict__['next-available']
return self._users.find_user_by_name(name)
def __is_available(self, next_available):
dt_next = timeutils.parse_isotime(next_available)
dt_now = datetime.now()
return dt_next.time() < dt_now.time()
@test(enabled=RD_CLIENT_OK)
def _get_limits_as_dict(self, limits):
d = {}
for l in limits:
d[l.verb] = l
return d
@test
def test_limits_index(self):
"""test_limits_index"""
r1, r2, r3, r4 = self.rd_client.limits.index()
assert_equal(r1.verb, "POST")
assert_equal(r1.unit, "MINUTE")
assert_true(r1.remaining <= DEFAULT_RATE)
limits = self.rd_client.limits.list()
d = self._get_limits_as_dict(limits)
next_available = self._get_next_available(r1)
assert_true(next_available is not None)
# remove the abs_limits from the rate limits
abs_limits = d.pop("ABSOLUTE", None)
assert_equal(abs_limits.verb, "ABSOLUTE")
assert_equal(int(abs_limits.maxTotalInstances), DEFAULT_MAX_INSTANCES)
assert_equal(int(abs_limits.maxTotalVolumes), DEFAULT_MAX_VOLUMES)
assert_equal(r2.verb, "PUT")
assert_equal(r2.unit, "MINUTE")
assert_true(r2.remaining <= DEFAULT_RATE)
for k in d:
assert_equal(d[k].verb, k)
assert_equal(d[k].unit, "MINUTE")
assert_true(int(d[k].remaining) <= DEFAULT_RATE)
assert_true(d[k].nextAvailable is not None)
next_available = self._get_next_available(r2)
assert_true(next_available is not None)
assert_equal(r3.verb, "DELETE")
assert_equal(r3.unit, "MINUTE")
assert_true(r3.remaining <= DEFAULT_RATE)
next_available = self._get_next_available(r3)
assert_true(next_available is not None)
assert_equal(r4.verb, "GET")
assert_equal(r4.unit, "MINUTE")
assert_true(r4.remaining <= DEFAULT_RATE)
next_available = self._get_next_available(r4)
assert_true(next_available is not None)
@test(enabled=RD_CLIENT_OK)
@test
def test_limits_get_remaining(self):
"""test_limits_get_remaining"""
gets = None
limits = ()
for i in xrange(5):
r1, r2, r3, r4 = self.rd_client.limits.index()
gets = r4
limits = self.rd_client.limits.list()
assert_equal(gets.verb, "GET")
assert_equal(gets.unit, "MINUTE")
assert_true(gets.remaining <= DEFAULT_RATE - 5)
d = self._get_limits_as_dict(limits)
abs_limits = d["ABSOLUTE"]
get = d["GET"]
next_available = self._get_next_available(gets)
assert_true(next_available is not None)
assert_equal(int(abs_limits.maxTotalInstances), DEFAULT_MAX_INSTANCES)
assert_equal(int(abs_limits.maxTotalVolumes), DEFAULT_MAX_VOLUMES)
assert_equal(get.verb, "GET")
assert_equal(get.unit, "MINUTE")
assert_true(int(get.remaining) <= DEFAULT_RATE - 5)
assert_true(get.nextAvailable is not None)
@test(enabled=RD_CLIENT_OK)
@test
def test_limits_exception(self):
"""test_limits_exception"""
@ -93,17 +108,24 @@ class Limits(object):
rate_user_exceeded = self._get_user('rate_limit_exceeded')
rd_client = create_dbaas_client(rate_user_exceeded)
gets = None
get = None
encountered = False
for i in xrange(DEFAULT_RATE + 50):
try:
r1, r2, r3, r4 = rd_client.limits.index()
gets = r4
assert_equal(gets.verb, "GET")
assert_equal(gets.unit, "MINUTE")
limits = rd_client.limits.list()
d = self._get_limits_as_dict(limits)
get = d["GET"]
abs_limits = d["ABSOLUTE"]
assert_equal(get.verb, "GET")
assert_equal(get.unit, "MINUTE")
assert_equal(int(abs_limits.maxTotalInstances),
DEFAULT_MAX_INSTANCES)
assert_equal(int(abs_limits.maxTotalVolumes),
DEFAULT_MAX_VOLUMES)
except exceptions.OverLimit:
encountered = True
assert_true(encountered)
assert_true(gets.remaining <= 50)
assert_true(int(get.remaining) <= 50)

View File

@ -20,31 +20,23 @@ Tests dealing with HTTP rate-limiting.
import httplib
import StringIO
from xml.dom import minidom
from lxml import etree
from reddwarf.quota.models import Quota
import testtools
import webob
from mockito import when
from mockito import when, mock, any
from reddwarf.common import limits
from reddwarf.common import xmlutil
from reddwarf.common.limits import Limit
from reddwarf.limits import views
from reddwarf.limits.service import LimitsController
from reddwarf.openstack.common import jsonutils
from reddwarf.tests.unittests.util.matchers import DictMatches
from reddwarf.quota.quota import QUOTAS
TEST_LIMITS = [
Limit("GET", "/delayed", "^/delayed", 1, limits.PER_MINUTE),
Limit("POST", "*", ".*", 7, limits.PER_MINUTE),
Limit("POST", "/servers", "^/servers", 3, limits.PER_MINUTE),
Limit("PUT", "*", "", 10, limits.PER_MINUTE),
Limit("PUT", "/servers", "^/servers", 5, limits.PER_MINUTE),
]
NS = {
'atom': 'http://www.w3.org/2005/Atom',
'ns': 'http://docs.openstack.org/common/api/v1.0'
}
class BaseLimitTestSuite(testtools.TestCase):
@ -52,19 +44,134 @@ class BaseLimitTestSuite(testtools.TestCase):
def setUp(self):
super(BaseLimitTestSuite, self).setUp()
self.absolute_limits = {}
self.absolute_limits = {"maxTotalInstances": 55,
"maxTotalVolumes": 100}
class LimitsControllerTest(BaseLimitTestSuite):
"""
Tests for `limits.LimitsController` class.
TODO: add test cases once absolute limits are integrated
"""
pass
def setUp(self):
super(LimitsControllerTest, self).setUp()
def test_limit_index_empty(self):
limit_controller = LimitsController()
req = mock()
req.environ = {}
when(QUOTAS).get_all_quotas_by_tenant(any()).thenReturn({})
view = limit_controller.index(req, "test_tenant_id")
expected = {'limits': [{'maxTotalInstances': 0,
'verb': 'ABSOLUTE', 'maxTotalVolumes': 0}]}
self.assertEqual(expected, view._data)
def test_limit_index(self):
tenant_id = "test_tenant_id"
limit_controller = LimitsController()
limits = [
{
"URI": "*",
"regex": ".*",
"value": 10,
"verb": "POST",
"remaining": 2,
"unit": "MINUTE",
"resetTime": 1311272226
},
{
"URI": "*",
"regex": ".*",
"value": 10,
"verb": "PUT",
"remaining": 2,
"unit": "MINUTE",
"resetTime": 1311272226
},
{
"URI": "*",
"regex": ".*",
"value": 10,
"verb": "DELETE",
"remaining": 2,
"unit": "MINUTE",
"resetTime": 1311272226
},
{
"URI": "*",
"regex": ".*",
"value": 10,
"verb": "GET",
"remaining": 2,
"unit": "MINUTE",
"resetTime": 1311272226
}
]
abs_limits = {"instances": Quota(tenant_id=tenant_id,
resource="instances",
hard_limit=100),
"volumes": Quota(tenant_id=tenant_id,
resource="volumes",
hard_limit=55)}
req = mock()
req.environ = {"reddwarf.limits": limits}
when(QUOTAS).get_all_quotas_by_tenant(tenant_id).thenReturn(abs_limits)
view = limit_controller.index(req, tenant_id)
expected = {
'limits': [
{
'maxTotalInstances': 100,
'verb': 'ABSOLUTE',
'maxTotalVolumes': 55
},
{
'regex': '.*',
'nextAvailable': '2011-07-21T18:17:06Z',
'uri': '*',
'value': 10,
'verb': 'POST',
'remaining': 2,
'unit': 'MINUTE'
},
{
'regex': '.*',
'nextAvailable': '2011-07-21T18:17:06Z',
'uri': '*',
'value': 10,
'verb': 'PUT',
'remaining': 2,
'unit': 'MINUTE'
},
{
'regex': '.*',
'nextAvailable': '2011-07-21T18:17:06Z',
'uri': '*',
'value': 10,
'verb': 'DELETE',
'remaining': 2,
'unit': 'MINUTE'
},
{
'regex': '.*',
'nextAvailable': '2011-07-21T18:17:06Z',
'uri': '*',
'value': 10,
'verb': 'GET',
'remaining': 2,
'unit': 'MINUTE'
}
]
}
self.assertEqual(expected, view._data)
class TestLimiter(limits.Limiter):
"""Note: This was taken from Nova"""
pass
@ -316,22 +423,6 @@ class LimiterTest(BaseLimitTestSuite):
self.assertEqual(expected, results)
def test_delay_PUT_servers(self):
"""
Ensure PUT on /servers limits at 5 requests, and PUT elsewhere is still
OK after 5 requests...but then after 11 total requests, PUT limiting
kicks in.
"""
# First 6 requests on PUT /servers
expected = [None] * 5 + [12.0]
results = list(self._check(6, "PUT", "/servers"))
self.assertEqual(expected, results)
# Next 5 request on PUT /anything
expected = [None] * 4 + [6.0]
results = list(self._check(5, "PUT", "/anything"))
self.assertEqual(expected, results)
def test_delay_PUT_wait(self):
"""
Ensure after hitting the limit and then waiting for the correct
@ -597,145 +688,149 @@ class WsgiLimiterProxyTest(BaseLimitTestSuite):
super(WsgiLimiterProxyTest, self).tearDown()
class LimitsViewBuilderTest(testtools.TestCase):
class LimitsViewTest(testtools.TestCase):
def setUp(self):
super(LimitsViewBuilderTest, self).setUp()
self.view_builder = views.ViewBuilder()
self.rate_limits = [{"URI": "*",
super(LimitsViewTest, self).setUp()
def test_empty_data(self):
"""
Test the default returned results if an empty dictionary is given
"""
rate_limit = {}
view = views.LimitView(rate_limit)
self.assertIsNotNone(view)
data = view.data()
expected = {'limit': {'regex': '',
'nextAvailable': '1970-01-01T00:00:00Z',
'uri': '',
'value': '',
'verb': '',
'remaining': 0,
'unit': ''}}
self.assertEqual(expected, data)
def test_data(self):
"""
Test the returned results for a fully populated dictionary
"""
rate_limit = {
"URI": "*",
"regex": ".*",
"value": 10,
"verb": "POST",
"remaining": 2,
"unit": "MINUTE",
"resetTime": 1311272226},
{"URI": "*/servers",
"regex": "^/servers",
"value": 50,
"verb": "POST",
"remaining": 10,
"unit": "DAY",
"resetTime": 1311272226}]
self.absolute_limits = {"metadata_items": 1,
"injected_files": 5,
"injected_file_content_bytes": 5}
"resetTime": 1311272226
}
def test_build_limits(self):
expected_limits = {"limits": {
"rate": [{"uri": "*",
"regex": ".*",
"limit": [{"value": 10,
"verb": "POST",
"remaining": 2,
"unit": "MINUTE",
"next-available": "2011-07-21T18:17:06Z"}]},
{"uri": "*/servers",
"regex": "^/servers",
"limit": [{"value": 50,
"verb": "POST",
"remaining": 10,
"unit": "DAY",
"next-available": "2011-07-21T18:17:06Z"}]}],
"absolute": {
"maxServerMeta": 1,
"maxImageMeta": 1,
"maxPersonality": 5,
"maxPersonalitySize": 5}}}
view = views.LimitView(rate_limit)
self.assertIsNotNone(view)
output = self.view_builder.build(self.rate_limits,
self.absolute_limits)
self.assertThat(output, DictMatches(expected_limits))
data = view.data()
expected = {'limit': {'regex': '.*',
'nextAvailable': '2011-07-21T18:17:06Z',
'uri': '*',
'value': 10,
'verb': 'POST',
'remaining': 2,
'unit': 'MINUTE'}}
def test_build_limits_empty_limits(self):
expected_limits = {"limits": {"rate": [],
"absolute": {}}}
self.assertEqual(expected, data)
abs_limits = {}
class LimitsViewsTest(testtools.TestCase):
def setUp(self):
super(LimitsViewsTest, self).setUp()
def test_empty_data(self):
rate_limits = []
output = self.view_builder.build(rate_limits, abs_limits)
self.assertThat(output, DictMatches(expected_limits))
abs_view = dict()
view_data = views.LimitViews(abs_view, rate_limits)
self.assertIsNotNone(view_data)
class LimitsXMLSerializationTest(testtools.TestCase):
def test_xml_declaration(self):
serializer = limits.LimitsTemplate()
data = view_data.data()
expected = {'limits': [{'maxTotalInstances': 0,
'verb': 'ABSOLUTE',
'maxTotalVolumes': 0}]}
fixture = {"limits": {
"rate": [],
"absolute": {}}}
self.assertEqual(expected, data)
output = serializer.serialize(fixture)
has_dec = output.startswith("<?xml version='1.0' encoding='UTF-8'?>")
self.assertTrue(has_dec)
def test_index(self):
serializer = limits.LimitsTemplate()
fixture = {
"limits": {
"rate": [{"uri": "*",
def test_data(self):
rate_limits = [
{
"URI": "*",
"regex": ".*",
"limit": [
{"value": 10,
"value": 10,
"verb": "POST",
"remaining": 2,
"unit": "MINUTE",
"next-available": "2011-12-15T22:42:45Z"}]},
{"uri": "*/servers",
"regex": "^/servers",
"limit": [
{"value": 50,
"verb": "POST",
"remaining": 10,
"unit": "DAY",
"next-available": "2011-12-15T22:42:45Z"}]}],
"absolute": {
"maxServerMeta": 1,
"maxImageMeta": 1,
"maxPersonality": 5,
"maxPersonalitySize": 10240}}}
"resetTime": 1311272226
},
{
"URI": "*",
"regex": ".*",
"value": 10,
"verb": "PUT",
"remaining": 2,
"unit": "MINUTE",
"resetTime": 1311272226
},
{
"URI": "*",
"regex": ".*",
"value": 10,
"verb": "DELETE",
"remaining": 2,
"unit": "MINUTE",
"resetTime": 1311272226
},
{
"URI": "*",
"regex": ".*",
"value": 10,
"verb": "GET",
"remaining": 2,
"unit": "MINUTE",
"resetTime": 1311272226
}
]
abs_view = {"instances": 55, "volumes": 100}
output = serializer.serialize(fixture)
root = etree.XML(output)
xmlutil.validate_schema(root, 'limits')
view_data = views.LimitViews(abs_view, rate_limits)
self.assertIsNotNone(view_data)
#verify absolute limits
absolutes = root.xpath('ns:absolute/ns:limit', namespaces=NS)
self.assertEqual(len(absolutes), 4)
for limit in absolutes:
name = limit.get('name')
value = limit.get('value')
self.assertEqual(value, str(fixture['limits']['absolute'][name]))
data = view_data.data()
expected = {'limits': [{'maxTotalInstances': 55,
'verb': 'ABSOLUTE',
'maxTotalVolumes': 100},
{'regex': '.*',
'nextAvailable': '2011-07-21T18:17:06Z',
'uri': '*',
'value': 10,
'verb': 'POST',
'remaining': 2, 'unit': 'MINUTE'},
{'regex': '.*',
'nextAvailable': '2011-07-21T18:17:06Z',
'uri': '*',
'value': 10,
'verb': 'PUT',
'remaining': 2,
'unit': 'MINUTE'},
{'regex': '.*',
'nextAvailable': '2011-07-21T18:17:06Z',
'uri': '*',
'value': 10,
'verb': 'DELETE',
'remaining': 2,
'unit': 'MINUTE'},
{'regex': '.*',
'nextAvailable': '2011-07-21T18:17:06Z',
'uri': '*',
'value': 10,
'verb': 'GET',
'remaining': 2, 'unit': 'MINUTE'}]}
#verify rate limits
rates = root.xpath('ns:rates/ns:rate', namespaces=NS)
self.assertEqual(len(rates), 2)
for i, rate in enumerate(rates):
for key in ['uri', 'regex']:
self.assertEqual(rate.get(key),
str(fixture['limits']['rate'][i][key]))
rate_limits = rate.xpath('ns:limit', namespaces=NS)
self.assertEqual(len(rate_limits), 1)
for j, limit in enumerate(rate_limits):
for key in ['verb', 'value', 'remaining', 'unit',
'next-available']:
self.assertEqual(limit.get(key),
str(fixture['limits']['rate'][i]['limit']
[j][key]))
def test_index_no_limits(self):
serializer = limits.LimitsTemplate()
fixture = {"limits": {
"rate": [],
"absolute": {}}}
output = serializer.serialize(fixture)
root = etree.XML(output)
xmlutil.validate_schema(root, 'limits')
#verify absolute limits
absolutes = root.xpath('ns:absolute/ns:limit', namespaces=NS)
self.assertEqual(len(absolutes), 0)
#verify rate limits
rates = root.xpath('ns:rates/ns:rate', namespaces=NS)
self.assertEqual(len(rates), 0)
self.assertEqual(expected, data)