Adds absolute limits to limit API call
Addresses XML issues Change-Id: I96df93c36c06baf309f881fc1f21b5acbd7fa953 Fixes: bug #1154298
This commit is contained in:
parent
2187352dad
commit
c27fd2447a
3
.gitignore
vendored
3
.gitignore
vendored
@ -21,3 +21,6 @@ Changelog
|
||||
reddwarf.iml
|
||||
atlassian-ide-plugin.xml
|
||||
.testrepository
|
||||
.pid
|
||||
.project
|
||||
.pydevproject
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -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"]
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
|
@ -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.
|
||||
|
@ -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
|
@ -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)
|
||||
|
@ -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}
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user