From c27fd2447a4bf9c290d457a193d00f6c4f29d890 Mon Sep 17 00:00:00 2001 From: daniel-a-nguyen Date: Tue, 12 Mar 2013 20:52:04 -0700 Subject: [PATCH] Adds absolute limits to limit API call Addresses XML issues Change-Id: I96df93c36c06baf309f881fc1f21b5acbd7fa953 Fixes: bug #1154298 --- .gitignore | 3 + etc/reddwarf/api-paste.ini.test | 5 +- etc/reddwarf/reddwarf.conf.test | 6 + etc/tests/localhost.test.conf | 18 - reddwarf/common/limits.py | 36 - reddwarf/common/xmlutil.py | 910 ------------------ reddwarf/limits/service.py | 28 +- reddwarf/limits/views.py | 101 +- reddwarf/tests/api/limits.py | 124 ++- .../tests/unittests/api/common/test_limits.py | 423 ++++---- 10 files changed, 384 insertions(+), 1270 deletions(-) delete mode 100644 reddwarf/common/xmlutil.py diff --git a/.gitignore b/.gitignore index ce1bcb681a..9ec1b5007f 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,6 @@ Changelog reddwarf.iml atlassian-ide-plugin.xml .testrepository +.pid +.project +.pydevproject \ No newline at end of file diff --git a/etc/reddwarf/api-paste.ini.test b/etc/reddwarf/api-paste.ini.test index edc97ee718..3415f2ed7d 100644 --- a/etc/reddwarf/api-paste.ini.test +++ b/etc/reddwarf/api-paste.ini.test @@ -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 diff --git a/etc/reddwarf/reddwarf.conf.test b/etc/reddwarf/reddwarf.conf.test index 11362b147d..87d5db7fb9 100644 --- a/etc/reddwarf/reddwarf.conf.test +++ b/etc/reddwarf/reddwarf.conf.test @@ -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 diff --git a/etc/tests/localhost.test.conf b/etc/tests/localhost.test.conf index d9efcd2757..cae44a17e2 100644 --- a/etc/tests/localhost.test.conf +++ b/etc/tests/localhost.test.conf @@ -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"] - } } ], diff --git a/reddwarf/common/limits.py b/reddwarf/common/limits.py index bfe233f6f6..2f830562f8 100644 --- a/reddwarf/common/limits.py +++ b/reddwarf/common/limits.py @@ -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. diff --git a/reddwarf/common/xmlutil.py b/reddwarf/common/xmlutil.py deleted file mode 100644 index 934da12ca1..0000000000 --- a/reddwarf/common/xmlutil.py +++ /dev/null @@ -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' % - (' '.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 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 diff --git a/reddwarf/limits/service.py b/reddwarf/limits/service.py index 3bb8a7b3e8..88e651a018 100644 --- a/reddwarf/limits/service.py +++ b/reddwarf/limits/service.py @@ -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) diff --git a/reddwarf/limits/views.py b/reddwarf/limits/views.py index 6158cc2bb7..db4840043e 100644 --- a/reddwarf/limits/views.py +++ b/reddwarf/limits/views.py @@ -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} diff --git a/reddwarf/tests/api/limits.py b/reddwarf/tests/api/limits.py index c22fa26ff7..4d64adfc13 100644 --- a/reddwarf/tests/api/limits.py +++ b/reddwarf/tests/api/limits.py @@ -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) diff --git a/reddwarf/tests/unittests/api/common/test_limits.py b/reddwarf/tests/unittests/api/common/test_limits.py index a7b19c7482..bc762a52eb 100644 --- a/reddwarf/tests/unittests/api/common/test_limits.py +++ b/reddwarf/tests/unittests/api/common/test_limits.py @@ -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": "*", - "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} + super(LimitsViewTest, self).setUp() - 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}}} + 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) - output = self.view_builder.build(self.rate_limits, - self.absolute_limits) - self.assertThat(output, DictMatches(expected_limits)) + data = view.data() + expected = {'limit': {'regex': '', + 'nextAvailable': '1970-01-01T00:00:00Z', + 'uri': '', + 'value': '', + 'verb': '', + 'remaining': 0, + 'unit': ''}} - def test_build_limits_empty_limits(self): - expected_limits = {"limits": {"rate": [], - "absolute": {}}} + self.assertEqual(expected, data) - 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 = [] - 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("") - self.assertTrue(has_dec) + def test_data(self): + rate_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_view = {"instances": 55, "volumes": 100} - def test_index(self): - serializer = limits.LimitsTemplate() - 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}}} + view_data = views.LimitViews(abs_view, rate_limits) + self.assertIsNotNone(view_data) - output = serializer.serialize(fixture) - root = etree.XML(output) - xmlutil.validate_schema(root, 'limits') + 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 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])) - - #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)