From b080169f94e9b3785a73da38a81a0ce302fcff37 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Thu, 2 Sep 2010 23:04:41 -0700 Subject: removed references to compute.model --- nova/api/rackspace/servers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'nova/api') diff --git a/nova/api/rackspace/servers.py b/nova/api/rackspace/servers.py index 25d1fe9c8..603a18944 100644 --- a/nova/api/rackspace/servers.py +++ b/nova/api/rackspace/servers.py @@ -16,9 +16,9 @@ # under the License. from nova import rpc -from nova.compute import model as compute from nova.api.rackspace import base +# FIXME(vish): convert from old usage of instance directory class Controller(base.Controller): entity_name = 'servers' -- cgit From c32beae895df87b8bac9fc4fed6bf73c19924b19 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Tue, 7 Sep 2010 20:03:07 -0700 Subject: first pass at cleanup rackspace/servers.py --- nova/api/rackspace/servers.py | 29 +++++++++++++---------------- 1 file changed, 13 insertions(+), 16 deletions(-) (limited to 'nova/api') diff --git a/nova/api/rackspace/servers.py b/nova/api/rackspace/servers.py index 603a18944..44174ca52 100644 --- a/nova/api/rackspace/servers.py +++ b/nova/api/rackspace/servers.py @@ -14,27 +14,31 @@ # 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 time +from nova import db +from nova import flags from nova import rpc +from nova import utils from nova.api.rackspace import base -# FIXME(vish): convert from old usage of instance directory +FLAGS = flags.FLAGS class Controller(base.Controller): entity_name = 'servers' def index(self, **kwargs): instances = [] - for inst in compute.InstanceDirectory().all: + for inst in db.instance_get_all(None): instances.append(instance_details(inst)) def show(self, **kwargs): instance_id = kwargs['id'] - return compute.InstanceDirectory().get(instance_id) + return db.instance_get(None, instance_id) def delete(self, **kwargs): instance_id = kwargs['id'] - instance = compute.InstanceDirectory().get(instance_id) + instance = db.instance_get(None, instance_id) if not instance: raise ServerNotFound("The requested server was not found") instance.destroy() @@ -45,11 +49,11 @@ class Controller(base.Controller): rpc.cast( FLAGS.compute_topic, { "method": "run_instance", - "args": {"instance_id": inst.instance_id}}) + "args": {"instance_id": inst['id']}}) def update(self, **kwargs): instance_id = kwargs['id'] - instance = compute.InstanceDirectory().get(instance_id) + instance = db.instance_get(None, instance_id) if not instance: raise ServerNotFound("The requested server was not found") instance.update(kwargs['server']) @@ -59,7 +63,7 @@ class Controller(base.Controller): """Build instance data structure and save it to the data store.""" reservation = utils.generate_uid('r') ltime = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime()) - inst = self.instdir.new() + inst = {} inst['name'] = env['server']['name'] inst['image_id'] = env['server']['imageId'] inst['instance_type'] = env['server']['flavorId'] @@ -68,15 +72,8 @@ class Controller(base.Controller): inst['reservation_id'] = reservation inst['launch_time'] = ltime inst['mac_address'] = utils.generate_mac() - address = self.network.allocate_ip( - inst['user_id'], - inst['project_id'], - mac=inst['mac_address']) - inst['private_dns_name'] = str(address) - inst['bridge_name'] = network.BridgedNetwork.get_network_for_project( - inst['user_id'], - inst['project_id'], - 'default')['bridge_name'] + inst_id = db.instance_create(None, inst) + address = self.network_manager.allocate_fixed_ip(None, inst_id) # key_data, key_name, ami_launch_index # TODO(todd): key data or root password inst.save() -- cgit From 6f5c16b62c441c97ade4f2f4b4878e8015c9281e Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Thu, 9 Sep 2010 21:52:06 -0700 Subject: make the db creates return refs instead of ids --- nova/api/rackspace/servers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'nova/api') diff --git a/nova/api/rackspace/servers.py b/nova/api/rackspace/servers.py index 44174ca52..1815f7523 100644 --- a/nova/api/rackspace/servers.py +++ b/nova/api/rackspace/servers.py @@ -72,7 +72,7 @@ class Controller(base.Controller): inst['reservation_id'] = reservation inst['launch_time'] = ltime inst['mac_address'] = utils.generate_mac() - inst_id = db.instance_create(None, inst) + inst_id = db.instance_create(None, inst)['id'] address = self.network_manager.allocate_fixed_ip(None, inst_id) # key_data, key_name, ami_launch_index # TODO(todd): key data or root password -- cgit From 3d68f1f74cd7fe6ddb9eec003a9e31f8ad036b27 Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Tue, 14 Sep 2010 16:26:19 -0400 Subject: Add ratelimiting package into Nova. After Austin it'll be pulled out into PyPI. --- nova/api/rackspace/ratelimiting/__init__.py | 103 ++++++++++++++++++++++++++++ nova/api/rackspace/ratelimiting/tests.py | 60 ++++++++++++++++ 2 files changed, 163 insertions(+) create mode 100644 nova/api/rackspace/ratelimiting/__init__.py create mode 100644 nova/api/rackspace/ratelimiting/tests.py (limited to 'nova/api') diff --git a/nova/api/rackspace/ratelimiting/__init__.py b/nova/api/rackspace/ratelimiting/__init__.py new file mode 100644 index 000000000..176e7d66e --- /dev/null +++ b/nova/api/rackspace/ratelimiting/__init__.py @@ -0,0 +1,103 @@ +"""Rate limiting of arbitrary actions.""" + +import time +import urllib +import webob.dec +import webob.exc + + +# Convenience constants for the limits dictionary passed to Limiter(). +PER_SECOND = 1 +PER_MINUTE = 60 +PER_HOUR = 60 * 60 +PER_DAY = 60 * 60 * 24 + +class Limiter(object): + + """Class providing rate limiting of arbitrary actions.""" + + def __init__(self, limits): + """Create a rate limiter. + + limits: a dict mapping from action name to a tuple. The tuple contains + the number of times the action may be performed, and the time period + (in seconds) during which the number must not be exceeded for this + action. Example: dict(reboot=(10, ratelimiting.PER_MINUTE)) would + allow 10 'reboot' actions per minute. + """ + self.limits = limits + self._levels = {} + + def perform(self, action_name, username='nobody'): + """Attempt to perform an action by the given username. + + action_name: the string name of the action to perform. This must + be a key in the limits dict passed to the ctor. + + username: an optional string name of the user performing the action. + Each user has her own set of rate limiting counters. Defaults to + 'nobody' (so that if you never specify a username when calling + perform(), a single set of counters will be used.) + + Return None if the action may proceed. If the action may not proceed + because it has been rate limited, return the float number of seconds + until the action would succeed. + """ + # Think of rate limiting as a bucket leaking water at 1cc/second. The + # bucket can hold as many ccs as there are seconds in the rate + # limiting period (e.g. 3600 for per-hour ratelimits), and if you can + # perform N actions in that time, each action fills the bucket by + # 1/Nth of its volume. You may only perform an action if the bucket + # would not overflow. + now = time.time() + key = '%s:%s' % (username, action_name) + last_time_performed, water_level = self._levels.get(key, (now, 0)) + # The bucket leaks 1cc/second. + water_level -= (now - last_time_performed) + if water_level < 0: + water_level = 0 + num_allowed_per_period, period_in_secs = self.limits[action_name] + # Fill the bucket by 1/Nth its capacity, and hope it doesn't overflow. + capacity = period_in_secs + new_level = water_level + (capacity * 1.0 / num_allowed_per_period) + if new_level > capacity: + # Delay this many seconds. + return new_level - capacity + self._levels[key] = (now, new_level) + return None + + +# If one instance of this WSGIApps is unable to handle your load, put a +# sharding app in front that shards by username to one of many backends. + +class WSGIApp(object): + + """Application that tracks rate limits in memory. Send requests to it of + this form: + + POST /limiter// + + and receive a 200 OK, or a 403 Forbidden with an X-Wait-Seconds header + containing the number of seconds to wait before the action would succeed. + """ + + def __init__(self, limiter): + """Create the WSGI application using the given Limiter instance.""" + self.limiter = limiter + + @webob.dec.wsgify + def __call__(req): + parts = req.path_info.split('/') + # format: /limiter// + if req.method != 'POST': + raise webob.exc.HTTPMethodNotAllowed() + if len(parts) != 4 or parts[1] != 'limiter': + raise webob.exc.HTTPNotFound() + username = parts[2] + action_name = urllib.unquote(parts[3]) + delay = self.limiter.perform(action_name, username) + if delay: + return webob.exc.HTTPForbidden( + headers={'X-Wait-Seconds': delay}) + else: + return '' # 200 OK diff --git a/nova/api/rackspace/ratelimiting/tests.py b/nova/api/rackspace/ratelimiting/tests.py new file mode 100644 index 000000000..1983cdea8 --- /dev/null +++ b/nova/api/rackspace/ratelimiting/tests.py @@ -0,0 +1,60 @@ +import ratelimiting +import time +import unittest + +class Test(unittest.TestCase): + + def setUp(self): + self.limits = { + 'a': (5, ratelimiting.PER_SECOND), + 'b': (5, ratelimiting.PER_MINUTE), + 'c': (5, ratelimiting.PER_HOUR), + 'd': (1, ratelimiting.PER_SECOND), + 'e': (100, ratelimiting.PER_SECOND)} + self.rl = ratelimiting.Limiter(self.limits) + + def exhaust(self, action, times_until_exhausted, **kwargs): + for i in range(times_until_exhausted): + when = self.rl.perform(action, **kwargs) + self.assertEqual(when, None) + num, period = self.limits[action] + delay = period * 1.0 / num + # Verify that we are now thoroughly delayed + for i in range(10): + when = self.rl.perform(action, **kwargs) + self.assertAlmostEqual(when, delay, 2) + + def test_second(self): + self.exhaust('a', 5) + time.sleep(0.2) + self.exhaust('a', 1) + time.sleep(1) + self.exhaust('a', 5) + + def test_minute(self): + self.exhaust('b', 5) + + def test_one_per_period(self): + def allow_once_and_deny_once(): + when = self.rl.perform('d') + self.assertEqual(when, None) + when = self.rl.perform('d') + self.assertAlmostEqual(when, 1, 2) + return when + time.sleep(allow_once_and_deny_once()) + time.sleep(allow_once_and_deny_once()) + allow_once_and_deny_once() + + def test_we_can_go_indefinitely_if_we_spread_out_requests(self): + for i in range(200): + when = self.rl.perform('e') + self.assertEqual(when, None) + time.sleep(0.01) + + def test_users_get_separate_buckets(self): + self.exhaust('c', 5, username='alice') + self.exhaust('c', 5, username='bob') + self.exhaust('c', 5, username='chuck') + self.exhaust('c', 0, username='chuck') + self.exhaust('c', 0, username='bob') + self.exhaust('c', 0, username='alice') -- cgit From 8138a35d3672e08640762b7533c1c527568d0b4f Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Tue, 14 Sep 2010 18:59:02 -0400 Subject: RateLimitingMiddleware --- nova/api/rackspace/__init__.py | 52 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) (limited to 'nova/api') diff --git a/nova/api/rackspace/__init__.py b/nova/api/rackspace/__init__.py index b4d666d63..e35109b43 100644 --- a/nova/api/rackspace/__init__.py +++ b/nova/api/rackspace/__init__.py @@ -31,6 +31,7 @@ from nova import flags from nova import wsgi from nova.api.rackspace import flavors from nova.api.rackspace import images +from nova.api.rackspace import ratelimiting from nova.api.rackspace import servers from nova.api.rackspace import sharedipgroups from nova.auth import manager @@ -40,7 +41,7 @@ class API(wsgi.Middleware): """WSGI entry point for all Rackspace API requests.""" def __init__(self): - app = AuthMiddleware(APIRouter()) + app = AuthMiddleware(RateLimitingMiddleware(APIRouter())) super(API, self).__init__(app) @@ -65,6 +66,55 @@ class AuthMiddleware(wsgi.Middleware): return self.application +class RateLimitingMiddleware(wsgi.Middleware): + """Rate limit incoming requests according to the OpenStack rate limits.""" + + def __init__(self, application): + super(RateLimitingMiddleware, self).__init__(application) + #TODO(gundlach): These limits were based on limitations of Cloud + #Servers. We should revisit them in Nova. + self.limiter = ratelimiting.Limiter(limits={ + 'DELETE': (100, ratelimiting.PER_MINUTE), + 'PUT': (10, ratelimiting.PER_MINUTE), + 'POST': (10, ratelimiting.PER_MINUTE), + 'POST servers': (50, ratelimiting.PER_DAY), + 'GET changes-since': (3, ratelimiting.PER_MINUTE), + }) + + @webob.dec.wsgify + def __call__(self, req): + """Rate limit the request. + + If the request should be rate limited, return a 413 status with a + Retry-After header giving the time when the request would succeed. + """ + username = req.headers['X-Auth-User'] + action_name = self.get_action_name(req) + if not action_name: # not rate limited + return self.application + delay = self.limiter.perform(action_name, username=username) + if action_name == 'POST servers': + # "POST servers" is a POST, so it counts against "POST" too. + delay2 = self.limiter.perform('POST', username=username) + delay = max(delay or 0, delay2 or 0) + if delay: + # TODO(gundlach): Get the retry-after format correct. + raise webob.exc.HTTPRequestEntityTooLarge(headers={ + 'Retry-After': time.time() + delay}) + else: + return self.application + + def get_action_name(self, req): + """Return the action name for this request.""" + if req.method == 'GET' and 'changes-since' in req.GET: + return 'GET changes-since' + if req.method == 'POST' and req.path_info.starts_with('/servers'): + return 'POST servers' + if req.method in ['PUT', 'POST', 'DELETE']: + return req.method + return None + + class APIRouter(wsgi.Router): """ Routes requests on the Rackspace API to the appropriate controller -- cgit From 63ad073efd0b20f59f02bc37182c0180cac3f405 Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Wed, 15 Sep 2010 09:25:53 -0400 Subject: RateLimitingMiddleware tests --- nova/api/rackspace/__init__.py | 24 ++++++++++++++++-------- nova/api/rackspace/ratelimiting/tests.py | 3 +++ 2 files changed, 19 insertions(+), 8 deletions(-) (limited to 'nova/api') diff --git a/nova/api/rackspace/__init__.py b/nova/api/rackspace/__init__.py index e35109b43..66d80a5b7 100644 --- a/nova/api/rackspace/__init__.py +++ b/nova/api/rackspace/__init__.py @@ -92,23 +92,31 @@ class RateLimitingMiddleware(wsgi.Middleware): action_name = self.get_action_name(req) if not action_name: # not rate limited return self.application - delay = self.limiter.perform(action_name, username=username) - if action_name == 'POST servers': - # "POST servers" is a POST, so it counts against "POST" too. - delay2 = self.limiter.perform('POST', username=username) - delay = max(delay or 0, delay2 or 0) + delay = self.get_delay(action_name, username) if delay: # TODO(gundlach): Get the retry-after format correct. raise webob.exc.HTTPRequestEntityTooLarge(headers={ 'Retry-After': time.time() + delay}) - else: - return self.application + return self.application + + def get_delay(self, action_name, username): + """Return the delay for the given action and username, or None if + the action would not be rate limited. + """ + if action_name == 'POST servers': + # "POST servers" is a POST, so it counts against "POST" too. + # Attempt the "POST" first, lest we are rate limited by "POST" but + # use up a precious "POST servers" call. + delay = self.limiter.perform("POST", username=username) + if delay: + return delay + return self.limiter.perform(action_name, username=username) def get_action_name(self, req): """Return the action name for this request.""" if req.method == 'GET' and 'changes-since' in req.GET: return 'GET changes-since' - if req.method == 'POST' and req.path_info.starts_with('/servers'): + if req.method == 'POST' and req.path_info.startswith('/servers'): return 'POST servers' if req.method in ['PUT', 'POST', 'DELETE']: return req.method diff --git a/nova/api/rackspace/ratelimiting/tests.py b/nova/api/rackspace/ratelimiting/tests.py index 1983cdea8..545e1d1b6 100644 --- a/nova/api/rackspace/ratelimiting/tests.py +++ b/nova/api/rackspace/ratelimiting/tests.py @@ -58,3 +58,6 @@ class Test(unittest.TestCase): self.exhaust('c', 0, username='chuck') self.exhaust('c', 0, username='bob') self.exhaust('c', 0, username='alice') + +if __name__ == '__main__': + unittest.main() -- cgit From fd4d5787d5b6f6e550d33c13eb76f4562a87a118 Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Wed, 15 Sep 2010 11:23:08 -0400 Subject: Test the WSGIApp --- nova/api/rackspace/ratelimiting/__init__.py | 2 +- nova/api/rackspace/ratelimiting/tests.py | 69 ++++++++++++++++++++++++++++- 2 files changed, 68 insertions(+), 3 deletions(-) (limited to 'nova/api') diff --git a/nova/api/rackspace/ratelimiting/__init__.py b/nova/api/rackspace/ratelimiting/__init__.py index 176e7d66e..64d5fff2c 100644 --- a/nova/api/rackspace/ratelimiting/__init__.py +++ b/nova/api/rackspace/ratelimiting/__init__.py @@ -86,7 +86,7 @@ class WSGIApp(object): self.limiter = limiter @webob.dec.wsgify - def __call__(req): + def __call__(self, req): parts = req.path_info.split('/') # format: /limiter// if req.method != 'POST': diff --git a/nova/api/rackspace/ratelimiting/tests.py b/nova/api/rackspace/ratelimiting/tests.py index 545e1d1b6..f924e7805 100644 --- a/nova/api/rackspace/ratelimiting/tests.py +++ b/nova/api/rackspace/ratelimiting/tests.py @@ -1,8 +1,10 @@ -import ratelimiting import time import unittest +import webob -class Test(unittest.TestCase): +import nova.api.rackspace.ratelimiting as ratelimiting + +class LimiterTest(unittest.TestCase): def setUp(self): self.limits = { @@ -59,5 +61,68 @@ class Test(unittest.TestCase): self.exhaust('c', 0, username='bob') self.exhaust('c', 0, username='alice') + +class WSGIAppTest(unittest.TestCase): + + def setUp(self): + test = self + class FakeLimiter(object): + def __init__(self): + self._action = self._username = self._delay = None + def mock(self, action, username, delay): + self._action = action + self._username = username + self._delay = delay + def perform(self, action, username): + test.assertEqual(action, self._action) + test.assertEqual(username, self._username) + return self._delay + self.limiter = FakeLimiter() + self.app = ratelimiting.WSGIApp(self.limiter) + + def test_invalid_methods(self): + requests = [] + for method in ['GET', 'PUT', 'DELETE']: + req = webob.Request.blank('/limits/michael/breakdance', + dict(REQUEST_METHOD=method)) + requests.append(req) + for req in requests: + self.assertEqual(req.get_response(self.app).status_int, 405) + + def test_invalid_urls(self): + requests = [] + for prefix in ['limit', '', 'limiter2', 'limiter/limits', 'limiter/1']: + req = webob.Request.blank('/%s/michael/breakdance' % prefix, + dict(REQUEST_METHOD='POST')) + requests.append(req) + for req in requests: + self.assertEqual(req.get_response(self.app).status_int, 404) + + def verify(self, url, username, action, delay=None): + """Make sure that POSTing to the given url causes the given username + to perform the given action. Make the internal rate limiter return + delay and make sure that the WSGI app returns the correct response. + """ + req = webob.Request.blank(url, dict(REQUEST_METHOD='POST')) + self.limiter.mock(action, username, delay) + resp = req.get_response(self.app) + if not delay: + self.assertEqual(resp.status_int, 200) + else: + self.assertEqual(resp.status_int, 403) + self.assertEqual(resp.headers['X-Wait-Seconds'], delay) + + def test_good_urls(self): + self.verify('/limiter/michael/hoot', 'michael', 'hoot') + + def test_escaping(self): + self.verify('/limiter/michael/jump%20up', 'michael', 'jump up') + + def test_response_to_delays(self): + self.verify('/limiter/michael/hoot', 'michael', 'hoot', 1) + self.verify('/limiter/michael/hoot', 'michael', 'hoot', 1.56) + self.verify('/limiter/michael/hoot', 'michael', 'hoot', 1000) + + if __name__ == '__main__': unittest.main() -- cgit From f200587ce068482ab94e777154de3ac777269fa0 Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Wed, 15 Sep 2010 13:54:38 -0400 Subject: Add support for middleware proxying to a ratelimiting.WSGIApp, for deployments that use more than one API Server and thus can't store ratelimiting counters in memory. --- nova/api/rackspace/__init__.py | 29 ++++-- nova/api/rackspace/ratelimiting/__init__.py | 21 ++++- nova/api/rackspace/ratelimiting/tests.py | 140 +++++++++++++++++++++++++--- 3 files changed, 165 insertions(+), 25 deletions(-) (limited to 'nova/api') diff --git a/nova/api/rackspace/__init__.py b/nova/api/rackspace/__init__.py index 66d80a5b7..ac5365310 100644 --- a/nova/api/rackspace/__init__.py +++ b/nova/api/rackspace/__init__.py @@ -69,17 +69,26 @@ class AuthMiddleware(wsgi.Middleware): class RateLimitingMiddleware(wsgi.Middleware): """Rate limit incoming requests according to the OpenStack rate limits.""" - def __init__(self, application): + def __init__(self, application, service_host=None): + """Create a rate limiting middleware that wraps the given application. + + By default, rate counters are stored in memory. If service_host is + specified, the middleware instead relies on the ratelimiting.WSGIApp + at the given host+port to keep rate counters. + """ super(RateLimitingMiddleware, self).__init__(application) - #TODO(gundlach): These limits were based on limitations of Cloud - #Servers. We should revisit them in Nova. - self.limiter = ratelimiting.Limiter(limits={ - 'DELETE': (100, ratelimiting.PER_MINUTE), - 'PUT': (10, ratelimiting.PER_MINUTE), - 'POST': (10, ratelimiting.PER_MINUTE), - 'POST servers': (50, ratelimiting.PER_DAY), - 'GET changes-since': (3, ratelimiting.PER_MINUTE), - }) + if not service_host: + #TODO(gundlach): These limits were based on limitations of Cloud + #Servers. We should revisit them in Nova. + self.limiter = ratelimiting.Limiter(limits={ + 'DELETE': (100, ratelimiting.PER_MINUTE), + 'PUT': (10, ratelimiting.PER_MINUTE), + 'POST': (10, ratelimiting.PER_MINUTE), + 'POST servers': (50, ratelimiting.PER_DAY), + 'GET changes-since': (3, ratelimiting.PER_MINUTE), + }) + else: + self.limiter = ratelimiting.WSGIAppProxy(service_host) @webob.dec.wsgify def __call__(self, req): diff --git a/nova/api/rackspace/ratelimiting/__init__.py b/nova/api/rackspace/ratelimiting/__init__.py index 64d5fff2c..f843bac0f 100644 --- a/nova/api/rackspace/ratelimiting/__init__.py +++ b/nova/api/rackspace/ratelimiting/__init__.py @@ -1,5 +1,6 @@ """Rate limiting of arbitrary actions.""" +import httplib import time import urllib import webob.dec @@ -98,6 +99,24 @@ class WSGIApp(object): delay = self.limiter.perform(action_name, username) if delay: return webob.exc.HTTPForbidden( - headers={'X-Wait-Seconds': delay}) + headers={'X-Wait-Seconds': "%.2f" % delay}) else: return '' # 200 OK + + +class WSGIAppProxy(object): + + """Limiter lookalike that proxies to a ratelimiting.WSGIApp.""" + + def __init__(self, service_host): + """Creates a proxy pointing to a ratelimiting.WSGIApp at the given + host.""" + self.service_host = service_host + + def perform(self, action, username='nobody'): + conn = httplib.HTTPConnection(self.service_host) + conn.request('POST', '/limiter/%s/%s' % (username, action)) + resp = conn.getresponse() + if resp.status == 200: + return None # no delay + return float(resp.getheader('X-Wait-Seconds')) diff --git a/nova/api/rackspace/ratelimiting/tests.py b/nova/api/rackspace/ratelimiting/tests.py index f924e7805..13a47989b 100644 --- a/nova/api/rackspace/ratelimiting/tests.py +++ b/nova/api/rackspace/ratelimiting/tests.py @@ -1,3 +1,5 @@ +import httplib +import StringIO import time import unittest import webob @@ -62,22 +64,25 @@ class LimiterTest(unittest.TestCase): self.exhaust('c', 0, username='alice') +class FakeLimiter(object): + """Fake Limiter class that you can tell how to behave.""" + def __init__(self, test): + self._action = self._username = self._delay = None + self.test = test + def mock(self, action, username, delay): + self._action = action + self._username = username + self._delay = delay + def perform(self, action, username): + self.test.assertEqual(action, self._action) + self.test.assertEqual(username, self._username) + return self._delay + + class WSGIAppTest(unittest.TestCase): def setUp(self): - test = self - class FakeLimiter(object): - def __init__(self): - self._action = self._username = self._delay = None - def mock(self, action, username, delay): - self._action = action - self._username = username - self._delay = delay - def perform(self, action, username): - test.assertEqual(action, self._action) - test.assertEqual(username, self._username) - return self._delay - self.limiter = FakeLimiter() + self.limiter = FakeLimiter(self) self.app = ratelimiting.WSGIApp(self.limiter) def test_invalid_methods(self): @@ -110,7 +115,7 @@ class WSGIAppTest(unittest.TestCase): self.assertEqual(resp.status_int, 200) else: self.assertEqual(resp.status_int, 403) - self.assertEqual(resp.headers['X-Wait-Seconds'], delay) + self.assertEqual(resp.headers['X-Wait-Seconds'], "%.2f" % delay) def test_good_urls(self): self.verify('/limiter/michael/hoot', 'michael', 'hoot') @@ -124,5 +129,112 @@ class WSGIAppTest(unittest.TestCase): self.verify('/limiter/michael/hoot', 'michael', 'hoot', 1000) +class FakeHttplibSocket(object): + """a fake socket implementation for httplib.HTTPResponse, trivial""" + + def __init__(self, response_string): + self._buffer = StringIO.StringIO(response_string) + + def makefile(self, _mode, _other): + """Returns the socket's internal buffer""" + return self._buffer + + +class FakeHttplibConnection(object): + """A fake httplib.HTTPConnection + + Requests made via this connection actually get translated and routed into + our WSGI app, we then wait for the response and turn it back into + an httplib.HTTPResponse. + """ + def __init__(self, app, host, is_secure=False): + self.app = app + self.host = host + + def request(self, method, path, data='', headers={}): + req = webob.Request.blank(path) + req.method = method + req.body = data + req.headers = headers + req.host = self.host + # Call the WSGI app, get the HTTP response + resp = str(req.get_response(self.app)) + # For some reason, the response doesn't have "HTTP/1.0 " prepended; I + # guess that's a function the web server usually provides. + resp = "HTTP/1.0 %s" % resp + sock = FakeHttplibSocket(resp) + self.http_response = httplib.HTTPResponse(sock) + self.http_response.begin() + + def getresponse(self): + return self.http_response + + +def wire_HTTPConnection_to_WSGI(host, app): + """Monkeypatches HTTPConnection so that if you try to connect to host, you + are instead routed straight to the given WSGI app. + + After calling this method, when any code calls + + httplib.HTTPConnection(host) + + the connection object will be a fake. Its requests will be sent directly + to the given WSGI app rather than through a socket. + + Code connecting to hosts other than host will not be affected. + + This method may be called multiple times to map different hosts to + different apps. + """ + class HTTPConnectionDecorator(object): + """Wraps the real HTTPConnection class so that when you instantiate + the class you might instead get a fake instance.""" + def __init__(self, wrapped): + self.wrapped = wrapped + def __call__(self, connection_host, *args, **kwargs): + if connection_host == host: + return FakeHttplibConnection(app, host) + else: + return self.wrapped(connection_host, *args, **kwargs) + httplib.HTTPConnection = HTTPConnectionDecorator(httplib.HTTPConnection) + + +class WSGIAppProxyTest(unittest.TestCase): + + def setUp(self): + """Our WSGIAppProxy is going to call across an HTTPConnection to a + WSGIApp running a limiter. The proxy will send input, and the proxy + should receive that same input, pass it to the limiter who gives a + result, and send the expected result back. + + The HTTPConnection isn't real -- it's monkeypatched to point straight + at the WSGIApp. And the limiter isn't real -- it's a fake that + behaves the way we tell it to. + """ + self.limiter = FakeLimiter(self) + app = ratelimiting.WSGIApp(self.limiter) + wire_HTTPConnection_to_WSGI('100.100.100.100:80', app) + self.proxy = ratelimiting.WSGIAppProxy('100.100.100.100:80') + + def test_200(self): + self.limiter.mock('conquer', 'caesar', None) + when = self.proxy.perform('conquer', 'caesar') + self.assertEqual(when, None) + + def test_403(self): + self.limiter.mock('grumble', 'proletariat', 1.5) + when = self.proxy.perform('grumble', 'proletariat') + self.assertEqual(when, 1.5) + + def test_failure(self): + self.limiter.mock('murder', 'brutus', None) + try: + when = self.proxy.perform('stab', 'brutus') + except AssertionError: + pass + else: + self.fail("I didn't perform the action I expected") + + if __name__ == '__main__': unittest.main() -- cgit From 7437df558f3277e21a4c34a5b517a1cae5dd5a74 Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Wed, 15 Sep 2010 17:17:20 -0400 Subject: Support querying version list --- nova/api/__init__.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) (limited to 'nova/api') diff --git a/nova/api/__init__.py b/nova/api/__init__.py index b9b9e3988..9f116dada 100644 --- a/nova/api/__init__.py +++ b/nova/api/__init__.py @@ -21,6 +21,7 @@ Root WSGI middleware for all API controllers. """ import routes +import webob.dec from nova import wsgi from nova.api import ec2 @@ -32,6 +33,18 @@ class API(wsgi.Router): def __init__(self): mapper = routes.Mapper() + mapper.connect("/", controller=self.versions) mapper.connect("/v1.0/{path_info:.*}", controller=rackspace.API()) mapper.connect("/ec2/{path_info:.*}", controller=ec2.API()) super(API, self).__init__(mapper) + + @webob.dec.wsgify + def versions(self, req): + """Respond to a request for all OpenStack API versions.""" + response = { + "versions": [ + dict(status="CURRENT", id="v1.0")]} + metadata = { + "application/xml": { + "attributes": dict(version=["status", "id"])}} + return wsgi.Serializer(req.environ, metadata).to_content_type(response) -- cgit From ae760b13c5382f2f4719dde445235c156cc27d18 Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Mon, 20 Sep 2010 14:49:05 -0400 Subject: Use assertRaises --- nova/api/rackspace/ratelimiting/tests.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) (limited to 'nova/api') diff --git a/nova/api/rackspace/ratelimiting/tests.py b/nova/api/rackspace/ratelimiting/tests.py index 13a47989b..4c9510917 100644 --- a/nova/api/rackspace/ratelimiting/tests.py +++ b/nova/api/rackspace/ratelimiting/tests.py @@ -227,13 +227,10 @@ class WSGIAppProxyTest(unittest.TestCase): self.assertEqual(when, 1.5) def test_failure(self): - self.limiter.mock('murder', 'brutus', None) - try: - when = self.proxy.perform('stab', 'brutus') - except AssertionError: - pass - else: - self.fail("I didn't perform the action I expected") + def shouldRaise(): + self.limiter.mock('murder', 'brutus', None) + self.proxy.perform('stab', 'brutus') + self.assertRaises(AssertionError, shouldRaise) if __name__ == '__main__': -- cgit