diff --git a/glance/common/client.py b/glance/common/client.py index 6d0fdf5ae6..5c4397aec5 100644 --- a/glance/common/client.py +++ b/glance/common/client.py @@ -521,6 +521,10 @@ def _chunkbody(connection, iter): raise TypeError('Unsupported image type: %s' % body.__class__) res = c.getresponse() + + def _retry(res): + return res.getheader('Retry-After') + status_code = self.get_status_code(res) if status_code in self.OK_RESPONSE_CODES: return res @@ -538,8 +542,13 @@ def _chunkbody(connection, iter): raise exception.Invalid(res.read()) elif status_code == httplib.MULTIPLE_CHOICES: raise exception.MultipleChoices(body=res.read()) + elif status_code == httplib.REQUEST_ENTITY_TOO_LARGE: + raise exception.LimitExceeded(retry=_retry(res), + body=res.read()) elif status_code == httplib.INTERNAL_SERVER_ERROR: raise Exception("Internal Server error: %s" % res.read()) + elif status_code == httplib.SERVICE_UNAVAILABLE: + raise exception.ServiceUnavailable(retry=_retry(res)) else: raise Exception("Unknown error occurred! %s" % res.read()) diff --git a/glance/common/exception.py b/glance/common/exception.py index 3ddf38fac9..03dc1616c9 100644 --- a/glance/common/exception.py +++ b/glance/common/exception.py @@ -143,6 +143,28 @@ class MultipleChoices(GlanceException): "request URI.\n\nThe body of response returned:\n%(body)s") +class LimitExceeded(GlanceException): + message = _("The request returned a 413 Request Entity Too Large. This " + "generally means that rate limiting or a quota threshold was " + "breached.\n\nThe response body:\n%(body)s") + + def __init__(self, *args, **kwargs): + self.retry_after = (int(kwargs['retry']) if kwargs.get('retry') + else None) + super(LimitExceeded, self).__init__(*args, **kwargs) + + +class ServiceUnavailable(GlanceException): + message = _("The request returned a 503 ServiceUnavilable. This " + "generally occurs on service overload or other transient " + "outage.") + + def __init__(self, *args, **kwargs): + self.retry_after = (int(kwargs['retry']) if kwargs.get('retry') + else None) + super(ServiceUnavailable, self).__init__(*args, **kwargs) + + class InvalidContentType(GlanceException): message = _("Invalid content type %(content_type)s") diff --git a/glance/tests/functional/test_client_exceptions.py b/glance/tests/functional/test_client_exceptions.py new file mode 100644 index 0000000000..dc9bfedcf5 --- /dev/null +++ b/glance/tests/functional/test_client_exceptions.py @@ -0,0 +1,101 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 OpenStack, LLC +# Copyright 2012 Red Hat, Inc +# 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. + +"""Functional test asserting strongly typed exceptions from glance client""" + +import eventlet.patcher +import webob.dec +import webob.exc + +from glance.common import client +from glance.common import exception +from glance.common import wsgi +from glance.tests import functional +from glance.tests import utils + + +eventlet.patcher.monkey_patch(socket=True) + + +class ExceptionTestApp(object): + """ + Test WSGI application which can respond with multiple kinds of HTTP + status codes + """ + + @webob.dec.wsgify + def __call__(self, request): + path = request.path_qs + + if path == "/rate-limit": + request.response = webob.exc.HTTPRequestEntityTooLarge() + + elif path == "/rate-limit-retry": + request.response.retry_after = 10 + request.response.status = 413 + + if path == "/service-unavailable": + request.response = webob.exc.HTTPServiceUnavailable() + + elif path == "/service-unavailable-retry": + request.response.retry_after = 10 + request.response.status = 503 + + +class TestClientExceptions(functional.FunctionalTest): + + def setUp(self): + super(TestClientExceptions, self).setUp() + self.port = utils.get_unused_port() + server = wsgi.Server() + conf = utils.TestConfigOpts({'bind_host': '127.0.0.1'}) + server.start(ExceptionTestApp(), conf, self.port) + self.client = client.BaseClient("127.0.0.1", self.port) + + def _do_test_exception(self, path, exc_type): + try: + self.client.do_request("GET", path) + self.fail('expected %s' % exc_type) + except exc_type as e: + self.assertEquals('retry' in path, e.retry_after == 10) + + def test_rate_limited(self): + """ + Test rate limited response + """ + self._do_test_exception('/rate-limit', exception.LimitExceeded) + + def test_rate_limited_retry(self): + """ + Test rate limited response with retry + """ + self._do_test_exception('/rate-limit-retry', exception.LimitExceeded) + + def test_service_unavailable(self): + """ + Test service unavailable response + """ + self._do_test_exception('/service-unavailable', + exception.ServiceUnavailable) + + def test_service_unavailable_retry(self): + """ + Test service unavailable response with retry + """ + self._do_test_exception('/service-unavailable-retry', + exception.ServiceUnavailable)