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
|
reddwarf.iml
|
||||||
atlassian-ide-plugin.xml
|
atlassian-ide-plugin.xml
|
||||||
.testrepository
|
.testrepository
|
||||||
|
.pid
|
||||||
|
.project
|
||||||
|
.pydevproject
|
@ -7,7 +7,7 @@ use = call:reddwarf.common.wsgi:versioned_urlmap
|
|||||||
paste.app_factory = reddwarf.versions:app_factory
|
paste.app_factory = reddwarf.versions:app_factory
|
||||||
|
|
||||||
[pipeline:reddwarfapi]
|
[pipeline:reddwarfapi]
|
||||||
pipeline = faultwrapper tokenauth authorization contextwrapper extensions reddwarfapp
|
pipeline = faultwrapper tokenauth authorization contextwrapper extensions ratelimit reddwarfapp
|
||||||
#pipeline = debug extensions reddwarfapp
|
#pipeline = debug extensions reddwarfapp
|
||||||
|
|
||||||
[filter:extensions]
|
[filter:extensions]
|
||||||
@ -34,6 +34,9 @@ paste.filter_factory = reddwarf.common.wsgi:ContextMiddleware.factory
|
|||||||
[filter:faultwrapper]
|
[filter:faultwrapper]
|
||||||
paste.filter_factory = reddwarf.common.wsgi:FaultWrapper.factory
|
paste.filter_factory = reddwarf.common.wsgi:FaultWrapper.factory
|
||||||
|
|
||||||
|
[filter:ratelimit]
|
||||||
|
paste.filter_factory = reddwarf.common.limits:RateLimitingMiddleware.factory
|
||||||
|
|
||||||
[app:reddwarfapp]
|
[app:reddwarfapp]
|
||||||
paste.app_factory = reddwarf.common.api:app_factory
|
paste.app_factory = reddwarf.common.api:app_factory
|
||||||
|
|
||||||
|
@ -69,6 +69,12 @@ max_instances_per_user = 55
|
|||||||
max_volumes_per_user = 100
|
max_volumes_per_user = 100
|
||||||
volume_time_out=30
|
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
|
# Auth
|
||||||
admin_roles = admin
|
admin_roles = admin
|
||||||
|
|
||||||
|
@ -45,24 +45,6 @@
|
|||||||
"is_admin":false,
|
"is_admin":false,
|
||||||
"services": ["reddwarf"]
|
"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 time
|
||||||
import webob.dec
|
import webob.dec
|
||||||
import webob.exc
|
import webob.exc
|
||||||
import xmlutil
|
|
||||||
|
|
||||||
from reddwarf.common import cfg
|
from reddwarf.common import cfg
|
||||||
from reddwarf.common import wsgi as base_wsgi
|
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 import wsgi
|
||||||
from reddwarf.openstack.common.gettextutils import _
|
from reddwarf.openstack.common.gettextutils import _
|
||||||
|
|
||||||
#
|
|
||||||
# TODO: come back to this later
|
|
||||||
# Dan Nguyen
|
|
||||||
#
|
|
||||||
#from nova import quota
|
|
||||||
#QUOTAS = quota.QUOTAS
|
|
||||||
|
|
||||||
|
|
||||||
CONF = cfg.CONF
|
CONF = cfg.CONF
|
||||||
|
|
||||||
@ -51,34 +43,6 @@ PER_HOUR = 60 * 60
|
|||||||
PER_DAY = 60 * 60 * 24
|
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):
|
class Limit(object):
|
||||||
"""
|
"""
|
||||||
Stores information about a limit for HTTP requests.
|
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
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
from reddwarf.common import wsgi as base_wsgi
|
from reddwarf.common import wsgi
|
||||||
from reddwarf.common.limits import LimitsTemplate
|
|
||||||
from reddwarf.limits import views
|
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.
|
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):
|
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]
|
quotas = QUOTAS.get_all_quotas_by_tenant(tenant_id)
|
||||||
|
abs_limits = dict((k, v['hard_limit']) for k, v in quotas.items())
|
||||||
#
|
|
||||||
# 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 = {}
|
|
||||||
rate_limits = req.environ.get("reddwarf.limits", [])
|
rate_limits = req.environ.get("reddwarf.limits", [])
|
||||||
|
|
||||||
builder = self._get_view_builder(req)
|
return wsgi.Result(views.LimitViews(abs_limits,
|
||||||
return builder.build(rate_limits, abs_limits)
|
rate_limits).data(), 200)
|
||||||
|
|
||||||
def _get_view_builder(self, req):
|
|
||||||
return views.ViewBuilder()
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||||
|
|
||||||
# Copyright 2010-2011 OpenStack LLC.
|
# Copyright 2013 OpenStack LLC.
|
||||||
# All Rights Reserved.
|
# All Rights Reserved.
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
@ -16,83 +16,44 @@
|
|||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
from reddwarf.openstack.common import timeutils
|
from reddwarf.openstack.common import timeutils
|
||||||
|
|
||||||
|
|
||||||
class ViewBuilder(object):
|
class LimitView(object):
|
||||||
"""OpenStack API base limits view builder."""
|
|
||||||
|
|
||||||
def build(self, rate_limits, absolute_limits):
|
def __init__(self, rate_limit):
|
||||||
rate_limits = self._build_rate_limits(rate_limits)
|
self.rate_limit = rate_limit
|
||||||
absolute_limits = self._build_absolute_limits(absolute_limits)
|
|
||||||
|
|
||||||
output = {
|
def data(self):
|
||||||
"limits": {
|
get_utc = datetime.datetime.utcfromtimestamp
|
||||||
"rate": rate_limits,
|
next_avail = get_utc(self.rate_limit.get("resetTime", 0))
|
||||||
"absolute": absolute_limits,
|
|
||||||
},
|
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):
|
class LimitViews(object):
|
||||||
"""Builder for absolute limits
|
|
||||||
|
|
||||||
absolute_limits should be given as a dict of limits.
|
def __init__(self, abs_limits, rate_limits):
|
||||||
For example: {"ram": 512, "gigabytes": 1024}.
|
self.abs_limits = abs_limits
|
||||||
|
self.rate_limits = rate_limits
|
||||||
|
|
||||||
"""
|
def data(self):
|
||||||
limit_names = {
|
data = []
|
||||||
"ram": ["maxTotalRAMSize"],
|
abs_view = dict()
|
||||||
"instances": ["maxTotalInstances"],
|
abs_view["verb"] = "ABSOLUTE"
|
||||||
"cores": ["maxTotalCores"],
|
abs_view["maxTotalInstances"] = self.abs_limits.get("instances", 0)
|
||||||
"metadata_items": ["maxServerMeta", "maxImageMeta"],
|
abs_view["maxTotalVolumes"] = self.abs_limits.get("volumes", 0)
|
||||||
"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 _build_rate_limits(self, rate_limits):
|
data.append(abs_view)
|
||||||
limits = []
|
for l in self.rate_limits:
|
||||||
for rate_limit in rate_limits:
|
data.append(LimitView(l).data()["limit"])
|
||||||
_rate_limit_key = None
|
return {"limits": data}
|
||||||
_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),
|
|
||||||
}
|
|
||||||
|
@ -7,15 +7,14 @@ from proboscis import test
|
|||||||
|
|
||||||
from reddwarf.openstack.common import timeutils
|
from reddwarf.openstack.common import timeutils
|
||||||
from reddwarf.tests.util import create_dbaas_client
|
from reddwarf.tests.util import create_dbaas_client
|
||||||
from reddwarf.tests.util import test_config
|
|
||||||
from reddwarfclient import exceptions
|
from reddwarfclient import exceptions
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from reddwarf.tests.util.users import Users
|
||||||
|
|
||||||
GROUP = "dbaas.api.limits"
|
GROUP = "dbaas.api.limits"
|
||||||
DEFAULT_RATE = 200
|
DEFAULT_RATE = 200
|
||||||
# Note: This should not be enabled until rd-client merges
|
DEFAULT_MAX_VOLUMES = 100
|
||||||
RD_CLIENT_OK = False
|
DEFAULT_MAX_INSTANCES = 55
|
||||||
|
|
||||||
|
|
||||||
@test(groups=[GROUP])
|
@test(groups=[GROUP])
|
||||||
@ -23,69 +22,85 @@ class Limits(object):
|
|||||||
|
|
||||||
@before_class
|
@before_class
|
||||||
def setUp(self):
|
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')
|
rate_user = self._get_user('rate_limit')
|
||||||
self.rd_client = create_dbaas_client(rate_user)
|
self.rd_client = create_dbaas_client(rate_user)
|
||||||
|
|
||||||
def _get_user(self, name):
|
def _get_user(self, name):
|
||||||
return test_config.users.find_user_by_name(name)
|
return self._users.find_user_by_name(name)
|
||||||
|
|
||||||
def _get_next_available(self, resource):
|
|
||||||
return resource.__dict__['next-available']
|
|
||||||
|
|
||||||
def __is_available(self, next_available):
|
def __is_available(self, next_available):
|
||||||
dt_next = timeutils.parse_isotime(next_available)
|
dt_next = timeutils.parse_isotime(next_available)
|
||||||
dt_now = datetime.now()
|
dt_now = datetime.now()
|
||||||
return dt_next.time() < dt_now.time()
|
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):
|
def test_limits_index(self):
|
||||||
"""test_limits_index"""
|
"""test_limits_index"""
|
||||||
r1, r2, r3, r4 = self.rd_client.limits.index()
|
|
||||||
|
|
||||||
assert_equal(r1.verb, "POST")
|
limits = self.rd_client.limits.list()
|
||||||
assert_equal(r1.unit, "MINUTE")
|
d = self._get_limits_as_dict(limits)
|
||||||
assert_true(r1.remaining <= DEFAULT_RATE)
|
|
||||||
|
|
||||||
next_available = self._get_next_available(r1)
|
# remove the abs_limits from the rate limits
|
||||||
assert_true(next_available is not None)
|
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")
|
for k in d:
|
||||||
assert_equal(r2.unit, "MINUTE")
|
assert_equal(d[k].verb, k)
|
||||||
assert_true(r2.remaining <= DEFAULT_RATE)
|
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)
|
@test
|
||||||
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)
|
|
||||||
def test_limits_get_remaining(self):
|
def test_limits_get_remaining(self):
|
||||||
"""test_limits_get_remaining"""
|
"""test_limits_get_remaining"""
|
||||||
gets = None
|
|
||||||
|
limits = ()
|
||||||
for i in xrange(5):
|
for i in xrange(5):
|
||||||
r1, r2, r3, r4 = self.rd_client.limits.index()
|
limits = self.rd_client.limits.list()
|
||||||
gets = r4
|
|
||||||
|
|
||||||
assert_equal(gets.verb, "GET")
|
d = self._get_limits_as_dict(limits)
|
||||||
assert_equal(gets.unit, "MINUTE")
|
abs_limits = d["ABSOLUTE"]
|
||||||
assert_true(gets.remaining <= DEFAULT_RATE - 5)
|
get = d["GET"]
|
||||||
|
|
||||||
next_available = self._get_next_available(gets)
|
assert_equal(int(abs_limits.maxTotalInstances), DEFAULT_MAX_INSTANCES)
|
||||||
assert_true(next_available is not None)
|
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):
|
def test_limits_exception(self):
|
||||||
"""test_limits_exception"""
|
"""test_limits_exception"""
|
||||||
|
|
||||||
@ -93,17 +108,24 @@ class Limits(object):
|
|||||||
rate_user_exceeded = self._get_user('rate_limit_exceeded')
|
rate_user_exceeded = self._get_user('rate_limit_exceeded')
|
||||||
rd_client = create_dbaas_client(rate_user_exceeded)
|
rd_client = create_dbaas_client(rate_user_exceeded)
|
||||||
|
|
||||||
gets = None
|
get = None
|
||||||
encountered = False
|
encountered = False
|
||||||
for i in xrange(DEFAULT_RATE + 50):
|
for i in xrange(DEFAULT_RATE + 50):
|
||||||
try:
|
try:
|
||||||
r1, r2, r3, r4 = rd_client.limits.index()
|
limits = rd_client.limits.list()
|
||||||
gets = r4
|
d = self._get_limits_as_dict(limits)
|
||||||
assert_equal(gets.verb, "GET")
|
get = d["GET"]
|
||||||
assert_equal(gets.unit, "MINUTE")
|
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:
|
except exceptions.OverLimit:
|
||||||
encountered = True
|
encountered = True
|
||||||
|
|
||||||
assert_true(encountered)
|
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 httplib
|
||||||
import StringIO
|
import StringIO
|
||||||
from xml.dom import minidom
|
from xml.dom import minidom
|
||||||
from lxml import etree
|
from reddwarf.quota.models import Quota
|
||||||
import testtools
|
import testtools
|
||||||
import webob
|
import webob
|
||||||
|
|
||||||
from mockito import when
|
from mockito import when, mock, any
|
||||||
|
|
||||||
from reddwarf.common import limits
|
from reddwarf.common import limits
|
||||||
from reddwarf.common import xmlutil
|
|
||||||
from reddwarf.common.limits import Limit
|
from reddwarf.common.limits import Limit
|
||||||
from reddwarf.limits import views
|
from reddwarf.limits import views
|
||||||
|
from reddwarf.limits.service import LimitsController
|
||||||
from reddwarf.openstack.common import jsonutils
|
from reddwarf.openstack.common import jsonutils
|
||||||
|
from reddwarf.quota.quota import QUOTAS
|
||||||
from reddwarf.tests.unittests.util.matchers import DictMatches
|
|
||||||
|
|
||||||
TEST_LIMITS = [
|
TEST_LIMITS = [
|
||||||
Limit("GET", "/delayed", "^/delayed", 1, limits.PER_MINUTE),
|
Limit("GET", "/delayed", "^/delayed", 1, limits.PER_MINUTE),
|
||||||
Limit("POST", "*", ".*", 7, 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", "*", "", 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):
|
class BaseLimitTestSuite(testtools.TestCase):
|
||||||
@ -52,19 +44,134 @@ class BaseLimitTestSuite(testtools.TestCase):
|
|||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super(BaseLimitTestSuite, self).setUp()
|
super(BaseLimitTestSuite, self).setUp()
|
||||||
|
self.absolute_limits = {"maxTotalInstances": 55,
|
||||||
self.absolute_limits = {}
|
"maxTotalVolumes": 100}
|
||||||
|
|
||||||
|
|
||||||
class LimitsControllerTest(BaseLimitTestSuite):
|
class LimitsControllerTest(BaseLimitTestSuite):
|
||||||
"""
|
def setUp(self):
|
||||||
Tests for `limits.LimitsController` class.
|
super(LimitsControllerTest, self).setUp()
|
||||||
TODO: add test cases once absolute limits are integrated
|
|
||||||
"""
|
def test_limit_index_empty(self):
|
||||||
pass
|
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):
|
class TestLimiter(limits.Limiter):
|
||||||
|
"""Note: This was taken from Nova"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
@ -316,22 +423,6 @@ class LimiterTest(BaseLimitTestSuite):
|
|||||||
|
|
||||||
self.assertEqual(expected, results)
|
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):
|
def test_delay_PUT_wait(self):
|
||||||
"""
|
"""
|
||||||
Ensure after hitting the limit and then waiting for the correct
|
Ensure after hitting the limit and then waiting for the correct
|
||||||
@ -597,145 +688,149 @@ class WsgiLimiterProxyTest(BaseLimitTestSuite):
|
|||||||
super(WsgiLimiterProxyTest, self).tearDown()
|
super(WsgiLimiterProxyTest, self).tearDown()
|
||||||
|
|
||||||
|
|
||||||
class LimitsViewBuilderTest(testtools.TestCase):
|
class LimitsViewTest(testtools.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super(LimitsViewBuilderTest, self).setUp()
|
super(LimitsViewTest, self).setUp()
|
||||||
self.view_builder = views.ViewBuilder()
|
|
||||||
self.rate_limits = [{"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}
|
|
||||||
|
|
||||||
def test_build_limits(self):
|
def test_empty_data(self):
|
||||||
expected_limits = {"limits": {
|
"""
|
||||||
"rate": [{"uri": "*",
|
Test the default returned results if an empty dictionary is given
|
||||||
"regex": ".*",
|
"""
|
||||||
"limit": [{"value": 10,
|
rate_limit = {}
|
||||||
"verb": "POST",
|
view = views.LimitView(rate_limit)
|
||||||
"remaining": 2,
|
self.assertIsNotNone(view)
|
||||||
"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}}}
|
|
||||||
|
|
||||||
output = self.view_builder.build(self.rate_limits,
|
data = view.data()
|
||||||
self.absolute_limits)
|
expected = {'limit': {'regex': '',
|
||||||
self.assertThat(output, DictMatches(expected_limits))
|
'nextAvailable': '1970-01-01T00:00:00Z',
|
||||||
|
'uri': '',
|
||||||
|
'value': '',
|
||||||
|
'verb': '',
|
||||||
|
'remaining': 0,
|
||||||
|
'unit': ''}}
|
||||||
|
|
||||||
def test_build_limits_empty_limits(self):
|
self.assertEqual(expected, data)
|
||||||
expected_limits = {"limits": {"rate": [],
|
|
||||||
"absolute": {}}}
|
|
||||||
|
|
||||||
abs_limits = {}
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
view = views.LimitView(rate_limit)
|
||||||
|
self.assertIsNotNone(view)
|
||||||
|
|
||||||
|
data = view.data()
|
||||||
|
expected = {'limit': {'regex': '.*',
|
||||||
|
'nextAvailable': '2011-07-21T18:17:06Z',
|
||||||
|
'uri': '*',
|
||||||
|
'value': 10,
|
||||||
|
'verb': 'POST',
|
||||||
|
'remaining': 2,
|
||||||
|
'unit': 'MINUTE'}}
|
||||||
|
|
||||||
|
self.assertEqual(expected, data)
|
||||||
|
|
||||||
|
|
||||||
|
class LimitsViewsTest(testtools.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
super(LimitsViewsTest, self).setUp()
|
||||||
|
|
||||||
|
def test_empty_data(self):
|
||||||
rate_limits = []
|
rate_limits = []
|
||||||
output = self.view_builder.build(rate_limits, abs_limits)
|
abs_view = dict()
|
||||||
self.assertThat(output, DictMatches(expected_limits))
|
|
||||||
|
|
||||||
|
view_data = views.LimitViews(abs_view, rate_limits)
|
||||||
|
self.assertIsNotNone(view_data)
|
||||||
|
|
||||||
class LimitsXMLSerializationTest(testtools.TestCase):
|
data = view_data.data()
|
||||||
def test_xml_declaration(self):
|
expected = {'limits': [{'maxTotalInstances': 0,
|
||||||
serializer = limits.LimitsTemplate()
|
'verb': 'ABSOLUTE',
|
||||||
|
'maxTotalVolumes': 0}]}
|
||||||
|
|
||||||
fixture = {"limits": {
|
self.assertEqual(expected, data)
|
||||||
"rate": [],
|
|
||||||
"absolute": {}}}
|
|
||||||
|
|
||||||
output = serializer.serialize(fixture)
|
def test_data(self):
|
||||||
has_dec = output.startswith("<?xml version='1.0' encoding='UTF-8'?>")
|
rate_limits = [
|
||||||
self.assertTrue(has_dec)
|
{
|
||||||
|
"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_view = {"instances": 55, "volumes": 100}
|
||||||
|
|
||||||
def test_index(self):
|
view_data = views.LimitViews(abs_view, rate_limits)
|
||||||
serializer = limits.LimitsTemplate()
|
self.assertIsNotNone(view_data)
|
||||||
fixture = {
|
|
||||||
"limits": {
|
|
||||||
"rate": [{"uri": "*",
|
|
||||||
"regex": ".*",
|
|
||||||
"limit": [
|
|
||||||
{"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}}}
|
|
||||||
|
|
||||||
output = serializer.serialize(fixture)
|
data = view_data.data()
|
||||||
root = etree.XML(output)
|
expected = {'limits': [{'maxTotalInstances': 55,
|
||||||
xmlutil.validate_schema(root, 'limits')
|
'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 absolute limits
|
self.assertEqual(expected, data)
|
||||||
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]))
|
|
||||||
|
|
||||||
#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)
|
|
||||||
|
Loading…
Reference in New Issue
Block a user