Do not cache images that fail checksum verfication
On an image GET, recalculate the image checksum as the image data is streamed to the client. Verify that the checksum matches the original checksum calculated when the image was added to Glance. If checksum validation fails, purge the image from the cache. This type of situation could occur if the backend image store is malfunctioning. bug 1028496 Change-Id: I9f38bac8360016bb12b5edaad87c50939a538cc0
This commit is contained in:
parent
e200f6fe24
commit
19334f0f8d
@ -164,7 +164,17 @@ class CacheFilter(wsgi.Middleware):
|
||||
return resp
|
||||
|
||||
def _process_GET_response(self, resp, image_id):
|
||||
resp.app_iter = self.cache.get_caching_iter(image_id, resp.app_iter)
|
||||
image_checksum = resp.headers.get('Content-MD5', None)
|
||||
|
||||
if not image_checksum:
|
||||
# API V1 stores the checksum in a different header:
|
||||
image_checksum = resp.headers.get('x-image-meta-checksum', None)
|
||||
|
||||
if not image_checksum:
|
||||
LOG.error(_("Checksum header is missing."))
|
||||
|
||||
resp.app_iter = self.cache.get_caching_iter(image_id, image_checksum,
|
||||
resp.app_iter)
|
||||
return resp
|
||||
|
||||
def get_status_code(self, response):
|
||||
|
@ -19,6 +19,8 @@
|
||||
LRU Cache for Image Data
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
|
||||
from glance.common import exception
|
||||
from glance.common import utils
|
||||
from glance.openstack.common import cfg
|
||||
@ -206,13 +208,15 @@ class ImageCache(object):
|
||||
"""
|
||||
return self.driver.queue_image(image_id)
|
||||
|
||||
def get_caching_iter(self, image_id, image_iter):
|
||||
def get_caching_iter(self, image_id, image_checksum, image_iter):
|
||||
"""
|
||||
Returns an iterator that caches the contents of an image
|
||||
while the image contents are read through the supplied
|
||||
iterator.
|
||||
|
||||
:param image_id: Image ID
|
||||
:param image_checksum: checksum expected to be generated while
|
||||
iterating over image data
|
||||
:param image_iter: Iterator that will read image contents
|
||||
"""
|
||||
if not self.driver.is_cacheable(image_id):
|
||||
@ -222,13 +226,23 @@ class ImageCache(object):
|
||||
|
||||
def tee_iter(image_id):
|
||||
try:
|
||||
current_checksum = hashlib.md5()
|
||||
|
||||
with self.driver.open_for_write(image_id) as cache_file:
|
||||
for chunk in image_iter:
|
||||
try:
|
||||
cache_file.write(chunk)
|
||||
finally:
|
||||
current_checksum.update(chunk)
|
||||
yield chunk
|
||||
cache_file.flush()
|
||||
|
||||
if image_checksum and \
|
||||
image_checksum != current_checksum.hexdigest():
|
||||
msg = _("Checksum verification failed. Aborted caching "
|
||||
"of image %s." % image_id)
|
||||
raise exception.GlanceException(msg)
|
||||
|
||||
except Exception:
|
||||
LOG.exception(_("Exception encountered while tee'ing "
|
||||
"image '%s' into cache. Continuing "
|
||||
|
@ -19,6 +19,16 @@ import glance.api.middleware.cache
|
||||
from glance.tests.unit import base
|
||||
|
||||
|
||||
class ChecksumTestCacheFilter(glance.api.middleware.cache.CacheFilter):
|
||||
def __init__(self):
|
||||
class DummyCache(object):
|
||||
def get_caching_iter(self, image_id, image_checksum,
|
||||
app_iter):
|
||||
self.image_checksum = image_checksum
|
||||
|
||||
self.cache = DummyCache()
|
||||
|
||||
|
||||
class TestCacheMiddleware(base.IsolatedUnitTest):
|
||||
def test_no_match_detail(self):
|
||||
req = webob.Request.blank('/v1/images/detail')
|
||||
@ -34,3 +44,29 @@ class TestCacheMiddleware(base.IsolatedUnitTest):
|
||||
req = webob.Request.blank('/v1/images/asdf?ping=pong')
|
||||
out = glance.api.middleware.cache.CacheFilter._match_request(req)
|
||||
self.assertEqual(out, ('v1', 'GET', 'asdf'))
|
||||
|
||||
def test_checksum_v1_header(self):
|
||||
cache_filter = ChecksumTestCacheFilter()
|
||||
headers = {"x-image-meta-checksum": "1234567890"}
|
||||
resp = webob.Response(headers=headers)
|
||||
cache_filter._process_GET_response(resp, None)
|
||||
|
||||
self.assertEqual("1234567890", cache_filter.cache.image_checksum)
|
||||
|
||||
def test_checksum_v2_header(self):
|
||||
cache_filter = ChecksumTestCacheFilter()
|
||||
headers = {
|
||||
"x-image-meta-checksum": "1234567890",
|
||||
"Content-MD5": "abcdefghi"
|
||||
}
|
||||
resp = webob.Response(headers=headers)
|
||||
cache_filter._process_GET_response(resp, None)
|
||||
|
||||
self.assertEqual("abcdefghi", cache_filter.cache.image_checksum)
|
||||
|
||||
def test_checksum_missing_header(self):
|
||||
cache_filter = ChecksumTestCacheFilter()
|
||||
resp = webob.Response()
|
||||
cache_filter._process_GET_response(resp, None)
|
||||
|
||||
self.assertEqual(None, cache_filter.cache.image_checksum)
|
||||
|
@ -16,6 +16,7 @@
|
||||
# under the License.
|
||||
|
||||
from contextlib import contextmanager
|
||||
import hashlib
|
||||
import os
|
||||
import random
|
||||
import shutil
|
||||
@ -23,6 +24,7 @@ import StringIO
|
||||
|
||||
import stubout
|
||||
|
||||
from glance.common import exception
|
||||
from glance.common import utils
|
||||
from glance import image_cache
|
||||
from glance.tests import utils as test_utils
|
||||
@ -274,7 +276,9 @@ class ImageCacheTestCase(object):
|
||||
# and a consuming iterator completes
|
||||
def consume(image_id):
|
||||
data = ['a', 'b', 'c', 'd', 'e', 'f']
|
||||
caching_iter = self.cache.get_caching_iter(image_id, iter(data))
|
||||
checksum = None
|
||||
caching_iter = self.cache.get_caching_iter(image_id, checksum,
|
||||
iter(data))
|
||||
self.assertEqual(list(caching_iter), data)
|
||||
|
||||
image_id = '1'
|
||||
@ -300,7 +304,9 @@ class ImageCacheTestCase(object):
|
||||
# test a case where a consuming iterator just stops.
|
||||
def falloffend(image_id):
|
||||
data = ['a', 'b', 'c', 'd', 'e', 'f']
|
||||
caching_iter = self.cache.get_caching_iter(image_id, iter(data))
|
||||
checksum = None
|
||||
caching_iter = self.cache.get_caching_iter(image_id, checksum,
|
||||
iter(data))
|
||||
self.assertEqual(caching_iter.next(), 'a')
|
||||
|
||||
image_id = '1'
|
||||
@ -406,6 +412,35 @@ class TestImageCacheSqlite(test_utils.BaseTestCase,
|
||||
if os.path.exists(self.cache_dir):
|
||||
shutil.rmtree(self.cache_dir)
|
||||
|
||||
def test_gate_caching_iter_good_checksum(self):
|
||||
image = "12345678990abcdefghijklmnop"
|
||||
image_id = 123
|
||||
|
||||
md5 = hashlib.md5()
|
||||
md5.update(image)
|
||||
checksum = md5.hexdigest()
|
||||
|
||||
cache = image_cache.ImageCache()
|
||||
img_iter = cache.get_caching_iter(image_id, checksum, image)
|
||||
for chunk in img_iter:
|
||||
pass
|
||||
# checksum is valid, fake image should be cached:
|
||||
self.assertTrue(cache.is_cached(image_id))
|
||||
|
||||
def test_gate_caching_iter_bad_checksum(self):
|
||||
image = "12345678990abcdefghijklmnop"
|
||||
image_id = 123
|
||||
checksum = "foobar" # bad.
|
||||
|
||||
cache = image_cache.ImageCache()
|
||||
img_iter = cache.get_caching_iter(image_id, checksum, image)
|
||||
|
||||
def reader():
|
||||
for chunk in img_iter:
|
||||
pass
|
||||
# checksum is invalid, caching will fail:
|
||||
self.assertFalse(cache.is_cached(image_id))
|
||||
|
||||
|
||||
class TestImageCacheNoDep(test_utils.BaseTestCase):
|
||||
|
||||
@ -445,7 +480,7 @@ class TestImageCacheNoDep(test_utils.BaseTestCase):
|
||||
cache = image_cache.ImageCache()
|
||||
data = ['a', 'b', 'c', 'Fail', 'd', 'e', 'f']
|
||||
|
||||
caching_iter = cache.get_caching_iter('dummy_id', iter(data))
|
||||
caching_iter = cache.get_caching_iter('dummy_id', None, iter(data))
|
||||
self.assertEqual(list(caching_iter), data)
|
||||
|
||||
def test_get_caching_iter_when_open_fails(self):
|
||||
@ -463,5 +498,5 @@ class TestImageCacheNoDep(test_utils.BaseTestCase):
|
||||
cache = image_cache.ImageCache()
|
||||
data = ['a', 'b', 'c', 'd', 'e', 'f']
|
||||
|
||||
caching_iter = cache.get_caching_iter('dummy_id', iter(data))
|
||||
caching_iter = cache.get_caching_iter('dummy_id', None, iter(data))
|
||||
self.assertEqual(list(caching_iter), data)
|
||||
|
Loading…
Reference in New Issue
Block a user