HTTP migrated
This commit is contained in:
parent
95d24563c3
commit
8149a7d0dd
|
@ -243,7 +243,7 @@ def get_size_from_backend(context, uri):
|
||||||
"""Retrieves image size from backend specified by uri"""
|
"""Retrieves image size from backend specified by uri"""
|
||||||
|
|
||||||
loc = location.get_location_from_uri(uri)
|
loc = location.get_location_from_uri(uri)
|
||||||
store = get_store_from_uri(context, uri, loc)
|
store = get_store_from_uri(uri)
|
||||||
|
|
||||||
return store.get_size(loc)
|
return store.get_size(loc)
|
||||||
|
|
||||||
|
@ -251,7 +251,7 @@ def get_size_from_backend(context, uri):
|
||||||
def delete_from_backend(context, uri, **kwargs):
|
def delete_from_backend(context, uri, **kwargs):
|
||||||
"""Removes chunks of data from backend specified by uri"""
|
"""Removes chunks of data from backend specified by uri"""
|
||||||
loc = location.get_location_from_uri(uri)
|
loc = location.get_location_from_uri(uri)
|
||||||
store = get_store_from_uri(context, uri, loc)
|
store = get_store_from_uri(uri)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return store.delete(loc)
|
return store.delete(loc)
|
||||||
|
|
|
@ -17,7 +17,8 @@ import httplib
|
||||||
import urlparse
|
import urlparse
|
||||||
|
|
||||||
from glance.store.common import exception
|
from glance.store.common import exception
|
||||||
import glance.store.openstack.common.log as logging
|
from glance.store.openstack.common.gettextutils import _
|
||||||
|
from glance.store.openstack.common import log as logging
|
||||||
import glance.store.base
|
import glance.store.base
|
||||||
import glance.store.location
|
import glance.store.location
|
||||||
|
|
||||||
|
@ -84,9 +85,8 @@ class StoreLocation(glance.store.location.StoreLocation):
|
||||||
else:
|
else:
|
||||||
self.user = None
|
self.user = None
|
||||||
if netloc == '':
|
if netloc == '':
|
||||||
reason = _("No address specified in HTTP URL")
|
LOG.debug(_("No address specified in HTTP URL"))
|
||||||
LOG.debug(reason)
|
raise exception.BadStoreUri(uri=uri)
|
||||||
raise exception.BadStoreUri(message=reason)
|
|
||||||
self.netloc = netloc
|
self.netloc = netloc
|
||||||
self.path = path
|
self.path = path
|
||||||
|
|
||||||
|
@ -153,7 +153,7 @@ class Store(glance.store.base.Store):
|
||||||
reason = (_("The HTTP URL exceeded %s maximum "
|
reason = (_("The HTTP URL exceeded %s maximum "
|
||||||
"redirects.") % MAX_REDIRECTS)
|
"redirects.") % MAX_REDIRECTS)
|
||||||
LOG.debug(reason)
|
LOG.debug(reason)
|
||||||
raise exception.MaxRedirectsExceeded(redirects=MAX_REDIRECTS)
|
raise exception.MaxRedirectsExceeded(message=reason)
|
||||||
loc = location.store_location
|
loc = location.store_location
|
||||||
conn_class = self._get_conn_class(loc)
|
conn_class = self._get_conn_class(loc)
|
||||||
conn = conn_class(loc.netloc)
|
conn = conn_class(loc.netloc)
|
||||||
|
@ -162,17 +162,19 @@ class Store(glance.store.base.Store):
|
||||||
|
|
||||||
# Check for bad status codes
|
# Check for bad status codes
|
||||||
if resp.status >= 400:
|
if resp.status >= 400:
|
||||||
reason = _("HTTP URL returned a %s status code.") % resp.status
|
reason = (_("HTTP URL %(url)s returned a %(status)s status code.") %
|
||||||
|
dict(url=loc.path, status=resp.status))
|
||||||
LOG.debug(reason)
|
LOG.debug(reason)
|
||||||
raise exception.BadStoreUri(loc.path, reason)
|
raise exception.BadStoreUri(message=reason)
|
||||||
|
|
||||||
location_header = resp.getheader("location")
|
location_header = resp.getheader("location")
|
||||||
if location_header:
|
if location_header:
|
||||||
if resp.status not in (301, 302):
|
if resp.status not in (301, 302):
|
||||||
reason = (_("The HTTP URL attempted to redirect with an "
|
reason = (_("The HTTP URL %(url)s attempted to redirect with an "
|
||||||
"invalid %s status code.") % resp.status)
|
"invalid %(status)s status code.") %
|
||||||
|
dict(url=loc.path, status=resp.status))
|
||||||
LOG.debug(reason)
|
LOG.debug(reason)
|
||||||
raise exception.BadStoreUri(loc.path, reason)
|
raise exception.BadStoreUri(message=reason)
|
||||||
location_class = glance.store.location.Location
|
location_class = glance.store.location.Location
|
||||||
new_loc = location_class(location.store_name,
|
new_loc = location_class(location.store_name,
|
||||||
location.store_location.__class__,
|
location.store_location.__class__,
|
||||||
|
|
|
@ -13,87 +13,53 @@
|
||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
from six.moves import xrange
|
import httplib
|
||||||
import stubout
|
import mock
|
||||||
|
import StringIO
|
||||||
|
|
||||||
from glance.store.common import exception
|
from glance.store.common import exception
|
||||||
from glance import context
|
from glance.store import delete_from_backend
|
||||||
from glance.db.sqlalchemy import api as db_api
|
from glance.store import safe_delete_from_backend
|
||||||
from glance.registry.client.v1.api import configure_registry_client
|
from glance.store._drivers import http
|
||||||
from glance.store import (delete_from_backend,
|
|
||||||
safe_delete_from_backend)
|
|
||||||
from glance.store.http import Store, MAX_REDIRECTS
|
|
||||||
from glance.store.location import get_location_from_uri
|
from glance.store.location import get_location_from_uri
|
||||||
from glance.tests.unit import base
|
from glance.store.tests import base
|
||||||
from glance.tests import utils, stubs as test_stubs
|
|
||||||
|
|
||||||
|
|
||||||
# The response stack is used to return designated responses in order;
|
class FakeHTTPResponse(object):
|
||||||
# however when it's empty a default 200 OK response is returned from
|
def __init__(self, status=200, headers=None, data=None, *args, **kwargs):
|
||||||
# FakeHTTPConnection below.
|
data = data or 'I am a teapot, short and stout\n'
|
||||||
FAKE_RESPONSE_STACK = []
|
self.data = StringIO.StringIO(data)
|
||||||
|
self.read = self.data.read
|
||||||
|
self.status = status
|
||||||
|
self.headers = headers or {'content-length': len(data)}
|
||||||
|
|
||||||
|
def getheader(self, name, default=None):
|
||||||
|
return self.headers.get(name.lower(), default)
|
||||||
|
|
||||||
|
def getheaders(self):
|
||||||
|
return self.headers or {}
|
||||||
|
|
||||||
|
def read(self, amt):
|
||||||
|
self.data.read(amt)
|
||||||
|
|
||||||
|
|
||||||
def stub_out_http_backend(stubs):
|
class TestHttpStore(base.StoreBaseTest):
|
||||||
"""
|
|
||||||
Stubs out the httplib.HTTPRequest.getresponse to return
|
|
||||||
faked-out data instead of grabbing actual contents of a resource
|
|
||||||
|
|
||||||
The stubbed getresponse() returns an iterator over
|
|
||||||
the data "I am a teapot, short and stout\n"
|
|
||||||
|
|
||||||
:param stubs: Set of stubout stubs
|
|
||||||
"""
|
|
||||||
|
|
||||||
class FakeHTTPConnection(object):
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def getresponse(self):
|
|
||||||
if len(FAKE_RESPONSE_STACK):
|
|
||||||
return FAKE_RESPONSE_STACK.pop()
|
|
||||||
return utils.FakeHTTPResponse()
|
|
||||||
|
|
||||||
def request(self, *_args, **_kwargs):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def close(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def fake_get_conn_class(self, *args, **kwargs):
|
|
||||||
return FakeHTTPConnection
|
|
||||||
|
|
||||||
stubs.Set(Store, '_get_conn_class', fake_get_conn_class)
|
|
||||||
|
|
||||||
|
|
||||||
def stub_out_registry_image_update(stubs):
|
|
||||||
"""
|
|
||||||
Stubs an image update on the registry.
|
|
||||||
|
|
||||||
:param stubs: Set of stubout stubs
|
|
||||||
"""
|
|
||||||
test_stubs.stub_out_registry_server(stubs)
|
|
||||||
|
|
||||||
def fake_image_update(ctx, image_id, values, purge_props=False):
|
|
||||||
return {'properties': {}}
|
|
||||||
|
|
||||||
stubs.Set(db_api, 'image_update', fake_image_update)
|
|
||||||
|
|
||||||
|
|
||||||
class TestHttpStore(base.StoreClearingUnitTest):
|
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
global FAKE_RESPONSE_STACK
|
|
||||||
FAKE_RESPONSE_STACK = []
|
|
||||||
self.config(default_store='http',
|
|
||||||
known_stores=['glance.store.http.Store'])
|
|
||||||
super(TestHttpStore, self).setUp()
|
super(TestHttpStore, self).setUp()
|
||||||
self.stubs = stubout.StubOutForTesting()
|
self.config(default_store='http', group='glance_store')
|
||||||
stub_out_http_backend(self.stubs)
|
http.Store.CHUNKSIZE = 2
|
||||||
Store.CHUNKSIZE = 2
|
self.store = http.Store(self.conf)
|
||||||
self.store = Store()
|
|
||||||
configure_registry_client()
|
response = mock.patch('httplib.HTTPConnection.getresponse')
|
||||||
|
self.response = response.start()
|
||||||
|
self.response.return_value = FakeHTTPResponse()
|
||||||
|
self.addCleanup(response.stop)
|
||||||
|
|
||||||
|
request = mock.patch('httplib.HTTPConnection.request')
|
||||||
|
self.request = request.start()
|
||||||
|
self.request.side_effect = lambda w, x, y, z: None
|
||||||
|
self.addCleanup(request.stop)
|
||||||
|
|
||||||
def test_http_get(self):
|
def test_http_get(self):
|
||||||
uri = "http://netloc/path/to/file.tar.gz"
|
uri = "http://netloc/path/to/file.tar.gz"
|
||||||
|
@ -109,18 +75,20 @@ class TestHttpStore(base.StoreClearingUnitTest):
|
||||||
# Add two layers of redirects to the response stack, which will
|
# Add two layers of redirects to the response stack, which will
|
||||||
# return the default 200 OK with the expected data after resolving
|
# return the default 200 OK with the expected data after resolving
|
||||||
# both redirects.
|
# both redirects.
|
||||||
redirect_headers_1 = {"location": "http://example.com/teapot.img"}
|
redirect1 = {"location": "http://example.com/teapot.img"}
|
||||||
redirect_resp_1 = utils.FakeHTTPResponse(status=302,
|
redirect2 = {"location": "http://example.com/teapot_real.img"}
|
||||||
headers=redirect_headers_1)
|
responses = [FakeHTTPResponse(status=302, headers=redirect1),
|
||||||
redirect_headers_2 = {"location": "http://example.com/teapot_real.img"}
|
FakeHTTPResponse(status=301, headers=redirect2),
|
||||||
redirect_resp_2 = utils.FakeHTTPResponse(status=301,
|
FakeHTTPResponse()]
|
||||||
headers=redirect_headers_2)
|
|
||||||
FAKE_RESPONSE_STACK.append(redirect_resp_1)
|
def getresponse():
|
||||||
FAKE_RESPONSE_STACK.append(redirect_resp_2)
|
return responses.pop()
|
||||||
|
self.response.side_effect = getresponse
|
||||||
|
|
||||||
uri = "http://netloc/path/to/file.tar.gz"
|
uri = "http://netloc/path/to/file.tar.gz"
|
||||||
expected_returns = ['I ', 'am', ' a', ' t', 'ea', 'po', 't,', ' s',
|
expected_returns = ['I ', 'am', ' a', ' t', 'ea', 'po', 't,', ' s',
|
||||||
'ho', 'rt', ' a', 'nd', ' s', 'to', 'ut', '\n']
|
'ho', 'rt', ' a', 'nd', ' s', 'to', 'ut', '\n']
|
||||||
|
|
||||||
loc = get_location_from_uri(uri)
|
loc = get_location_from_uri(uri)
|
||||||
(image_file, image_size) = self.store.get(loc)
|
(image_file, image_size) = self.store.get(loc)
|
||||||
self.assertEqual(image_size, 31)
|
self.assertEqual(image_size, 31)
|
||||||
|
@ -129,60 +97,45 @@ class TestHttpStore(base.StoreClearingUnitTest):
|
||||||
self.assertEqual(chunks, expected_returns)
|
self.assertEqual(chunks, expected_returns)
|
||||||
|
|
||||||
def test_http_get_max_redirects(self):
|
def test_http_get_max_redirects(self):
|
||||||
# Add more than MAX_REDIRECTS redirects to the response stack
|
redirect = {"location": "http://example.com/teapot.img"}
|
||||||
redirect_headers = {"location": "http://example.com/teapot.img"}
|
responses = ([FakeHTTPResponse(status=302, headers=redirect)]
|
||||||
redirect_resp = utils.FakeHTTPResponse(status=302,
|
* (http.MAX_REDIRECTS + 2))
|
||||||
headers=redirect_headers)
|
|
||||||
for i in xrange(MAX_REDIRECTS + 2):
|
def getresponse():
|
||||||
FAKE_RESPONSE_STACK.append(redirect_resp)
|
return responses.pop()
|
||||||
|
self.response.side_effect = getresponse
|
||||||
|
|
||||||
uri = "http://netloc/path/to/file.tar.gz"
|
uri = "http://netloc/path/to/file.tar.gz"
|
||||||
loc = get_location_from_uri(uri)
|
loc = get_location_from_uri(uri)
|
||||||
self.assertRaises(exception.MaxRedirectsExceeded, self.store.get, loc)
|
self.assertRaises(exception.MaxRedirectsExceeded, self.store.get, loc)
|
||||||
|
|
||||||
def test_http_get_redirect_invalid(self):
|
def test_http_get_redirect_invalid(self):
|
||||||
redirect_headers = {"location": "http://example.com/teapot.img"}
|
redirect = {"location": "http://example.com/teapot.img"}
|
||||||
redirect_resp = utils.FakeHTTPResponse(status=307,
|
redirect_resp = FakeHTTPResponse(status=307, headers=redirect)
|
||||||
headers=redirect_headers)
|
self.response.return_value = redirect_resp
|
||||||
FAKE_RESPONSE_STACK.append(redirect_resp)
|
|
||||||
|
|
||||||
uri = "http://netloc/path/to/file.tar.gz"
|
uri = "http://netloc/path/to/file.tar.gz"
|
||||||
loc = get_location_from_uri(uri)
|
loc = get_location_from_uri(uri)
|
||||||
self.assertRaises(exception.BadStoreUri, self.store.get, loc)
|
self.assertRaises(exception.BadStoreUri, self.store.get, loc)
|
||||||
|
|
||||||
def test_http_get_not_found(self):
|
def test_http_get_not_found(self):
|
||||||
not_found_resp = utils.FakeHTTPResponse(status=404,
|
fake = FakeHTTPResponse(status=404, data="404 Not Found")
|
||||||
data="404 Not Found")
|
self.response.return_value = fake
|
||||||
FAKE_RESPONSE_STACK.append(not_found_resp)
|
|
||||||
|
|
||||||
uri = "http://netloc/path/to/file.tar.gz"
|
uri = "http://netloc/path/to/file.tar.gz"
|
||||||
loc = get_location_from_uri(uri)
|
loc = get_location_from_uri(uri)
|
||||||
self.assertRaises(exception.BadStoreUri, self.store.get, loc)
|
self.assertRaises(exception.BadStoreUri, self.store.get, loc)
|
||||||
|
|
||||||
def test_https_get(self):
|
|
||||||
uri = "https://netloc/path/to/file.tar.gz"
|
|
||||||
expected_returns = ['I ', 'am', ' a', ' t', 'ea', 'po', 't,', ' s',
|
|
||||||
'ho', 'rt', ' a', 'nd', ' s', 'to', 'ut', '\n']
|
|
||||||
loc = get_location_from_uri(uri)
|
|
||||||
(image_file, image_size) = self.store.get(loc)
|
|
||||||
self.assertEqual(image_size, 31)
|
|
||||||
|
|
||||||
chunks = [c for c in image_file]
|
|
||||||
self.assertEqual(chunks, expected_returns)
|
|
||||||
|
|
||||||
def test_http_delete_raise_error(self):
|
def test_http_delete_raise_error(self):
|
||||||
uri = "https://netloc/path/to/file.tar.gz"
|
uri = "https://netloc/path/to/file.tar.gz"
|
||||||
loc = get_location_from_uri(uri)
|
loc = get_location_from_uri(uri)
|
||||||
ctx = context.RequestContext()
|
|
||||||
self.assertRaises(NotImplementedError, self.store.delete, loc)
|
self.assertRaises(NotImplementedError, self.store.delete, loc)
|
||||||
self.assertRaises(exception.StoreDeleteNotSupported,
|
self.assertRaises(exception.StoreDeleteNotSupported,
|
||||||
delete_from_backend, ctx, uri)
|
delete_from_backend, {}, uri)
|
||||||
|
|
||||||
def test_http_schedule_delete_swallows_error(self):
|
def test_http_schedule_delete_swallows_error(self):
|
||||||
uri = "https://netloc/path/to/file.tar.gz"
|
uri = "https://netloc/path/to/file.tar.gz"
|
||||||
ctx = context.RequestContext()
|
|
||||||
stub_out_registry_image_update(self.stubs)
|
|
||||||
try:
|
try:
|
||||||
safe_delete_from_backend(ctx, uri, 'image_id')
|
safe_delete_from_backend({}, uri, 'image_id')
|
||||||
except exception.StoreDeleteNotSupported:
|
except exception.StoreDeleteNotSupported:
|
||||||
self.fail('StoreDeleteNotSupported should be swallowed')
|
self.fail('StoreDeleteNotSupported should be swallowed')
|
||||||
|
|
Loading…
Reference in New Issue