Merge "Handling HTTP range requests in Glance"

This commit is contained in:
Jenkins 2017-01-05 23:49:21 +00:00 committed by Gerrit Code Review
commit 6d7b15c4e4
5 changed files with 79 additions and 11 deletions

View File

@ -18,6 +18,7 @@ from oslo_config import cfg
from oslo_log import log as logging
from oslo_utils import encodeutils
from oslo_utils import excutils
import six
import webob.exc
import glance.api.policy
@ -282,17 +283,20 @@ class ResponseSerializer(wsgi.JSONResponseSerializer):
def download(self, response, image):
offset, chunk_size = 0, None
range_val = response.request.get_content_range()
# NOTE(dharinic): In case of a malformed content range,
# glance/common/wsgi.py will raise HTTPRequestRangeNotSatisfiable
# (setting status_code to 416)
range_val = response.request.get_content_range(image.size)
if range_val:
# NOTE(flaper87): if not present, both, start
# and stop, will be None.
if range_val.start is not None:
if range_val.start is not None and range_val.stop is not None:
offset = range_val.start
if range_val.stop is not None:
chunk_size = range_val.stop - offset
response.status_int = 206
response.headers['Content-Type'] = 'application/octet-stream'
try:
@ -301,6 +305,11 @@ class ResponseSerializer(wsgi.JSONResponseSerializer):
# an iterator very strange
response.app_iter = iter(image.get_data(offset=offset,
chunk_size=chunk_size))
# NOTE(dharinic): In case of a full image download, when
# chunk_size was none, reset it to image.size to set the
# response header's Content-Length.
if not chunk_size:
chunk_size = image.size
except glance_store.NotFound as e:
raise webob.exc.HTTPNoContent(explanation=e.msg)
except glance_store.RemoteServiceUnavailable as e:
@ -318,7 +327,7 @@ class ResponseSerializer(wsgi.JSONResponseSerializer):
response.headers['Content-MD5'] = image.checksum
# NOTE(markwash): "response.app_iter = ..." also erroneously resets the
# content-length
response.headers['Content-Length'] = str(image.size)
response.headers['Content-Length'] = six.text_type(chunk_size)
def upload(self, response, result):
response.status_int = 204

View File

@ -972,14 +972,17 @@ class Request(webob.Request):
langs = i18n.get_available_languages('glance')
return self.accept_language.best_match(langs)
def get_content_range(self):
def get_content_range(self, image_size):
"""Return the `Range` in a request."""
range_str = self.headers.get('Content-Range')
if range_str is not None:
range_ = webob.byterange.ContentRange.parse(range_str)
if range_ is None:
# NOTE(dharinic): Ensure that a range like 1-4/* for an image
# size of 3 is invalidated.
if range_ is None or (range_.length is None and
range_.stop > image_size):
msg = _('Malformed Content-Range header: %s') % range_str
raise webob.exc.HTTPBadRequest(explanation=msg)
raise webob.exc.HTTPRequestRangeNotSatisfiable(explanation=msg)
return range_

View File

@ -771,10 +771,18 @@ class TestImages(functional.FunctionalTest):
headers = self._headers({'Content-Range': content_range})
path = self._url('/v2/images/%s/file' % image_id)
response = requests.get(path, headers=headers)
self.assertEqual(206, response.status_code)
result_body += response.text
self.assertEqual(result_body, image_data)
# test for failure on unsatisfiable request range.
content_range = 'bytes 3-16/15'
headers = self._headers({'Content-Range': content_range})
path = self._url('/v2/images/%s/file' % image_id)
response = requests.get(path, headers=headers)
self.assertEqual(416, response.status_code)
self.stop_servers()
def test_download_policy_when_cache_is_not_enabled(self):

View File

@ -67,7 +67,7 @@ class RequestTest(test_utils.BaseTestCase):
def test_content_range(self):
request = wsgi.Request.blank('/tests/123')
request.headers["Content-Range"] = 'bytes 10-99/*'
range_ = request.get_content_range()
range_ = request.get_content_range(120)
self.assertEqual(10, range_.start)
self.assertEqual(100, range_.stop) # non-inclusive
self.assertIsNone(range_.length)
@ -75,8 +75,8 @@ class RequestTest(test_utils.BaseTestCase):
def test_content_range_invalid(self):
request = wsgi.Request.blank('/tests/123')
request.headers["Content-Range"] = 'bytes=0-99'
self.assertRaises(webob.exc.HTTPBadRequest,
request.get_content_range)
self.assertRaises(webob.exc.HTTPRequestRangeNotSatisfiable,
request.get_content_range, 120)
def test_content_type_missing(self):
request = wsgi.Request.blank('/tests/123')

View File

@ -548,6 +548,54 @@ class TestImageDataSerializer(test_utils.BaseTestCase):
self.assertEqual('application/octet-stream',
response.headers['Content-Type'])
def _test_partial_download_successful(self, d_range):
request = wsgi.Request.blank('/')
request.environ = {}
request.headers['Content-Range'] = d_range
response = webob.Response()
response.request = request
image = FakeImage(size=3, data=[b'Z', b'Z', b'Z'])
self.serializer.download(response, image)
self.assertEqual(206, response.status_code)
self.assertEqual('2', response.headers['Content-Length'])
def test_partial_download_successful_with_range(self):
self._test_partial_download_successful('bytes 1-2/3')
self._test_partial_download_successful('bytes 1-2/*')
def _test_partial_download_failures(self, d_range):
request = wsgi.Request.blank('/')
request.environ = {}
request.headers['Content-Range'] = d_range
response = webob.Response()
response.request = request
image = FakeImage(size=3, data=[b'Z', b'Z', b'Z'])
self.assertRaises(webob.exc.HTTPRequestRangeNotSatisfiable,
self.serializer.download,
response, image)
return
def test_partial_download_failure_with_range(self):
self._test_partial_download_failures('bytes 1-4/3')
self._test_partial_download_failures('bytes 1-4/*')
self._test_partial_download_failures('bytes 4-1/3')
self._test_partial_download_failures('bytes 4-1/*')
def test_download_failure_with_valid_range(self):
with mock.patch.object(glance.api.policy.ImageProxy,
'get_data') as mock_get_data:
mock_get_data.side_effect = glance_store.NotFound(image="image")
request = wsgi.Request.blank('/')
request.environ = {}
request.headers['Content-Range'] = 'bytes %s-%s/3' % (1, 2)
response = webob.Response()
response.request = request
image = FakeImage(size=3, data=[b'Z', b'Z', b'Z'])
image.get_data = mock_get_data
self.assertRaises(webob.exc.HTTPNoContent,
self.serializer.download,
response, image)
def test_download_with_checksum(self):
request = wsgi.Request.blank('/')
request.environ = {}