233 lines
8.2 KiB
Python
233 lines
8.2 KiB
Python
# Copyright 2010 OpenStack Foundation
|
|
# 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.
|
|
|
|
import logging
|
|
import socket
|
|
|
|
from six.moves import http_client
|
|
from six.moves import urllib
|
|
|
|
from glance_store import capabilities
|
|
import glance_store.driver
|
|
from glance_store import exceptions
|
|
from glance_store.i18n import _
|
|
from glance_store.i18n import _LE
|
|
import glance_store.location
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
|
|
|
|
MAX_REDIRECTS = 5
|
|
|
|
|
|
class StoreLocation(glance_store.location.StoreLocation):
|
|
|
|
"""Class describing an HTTP(S) URI."""
|
|
|
|
def process_specs(self):
|
|
self.scheme = self.specs.get('scheme', 'http')
|
|
self.netloc = self.specs['netloc']
|
|
self.user = self.specs.get('user')
|
|
self.password = self.specs.get('password')
|
|
self.path = self.specs.get('path')
|
|
|
|
def _get_credstring(self):
|
|
if self.user:
|
|
return '%s:%s@' % (self.user, self.password)
|
|
return ''
|
|
|
|
def get_uri(self):
|
|
return "%s://%s%s%s" % (
|
|
self.scheme,
|
|
self._get_credstring(),
|
|
self.netloc,
|
|
self.path)
|
|
|
|
def parse_uri(self, uri):
|
|
"""
|
|
Parse URLs. This method fixes an issue where credentials specified
|
|
in the URL are interpreted differently in Python 2.6.1+ than prior
|
|
versions of Python.
|
|
"""
|
|
pieces = urllib.parse.urlparse(uri)
|
|
assert pieces.scheme in ('https', 'http')
|
|
self.scheme = pieces.scheme
|
|
netloc = pieces.netloc
|
|
path = pieces.path
|
|
try:
|
|
if '@' in netloc:
|
|
creds, netloc = netloc.split('@')
|
|
else:
|
|
creds = None
|
|
except ValueError:
|
|
# Python 2.6.1 compat
|
|
# see lp659445 and Python issue7904
|
|
if '@' in path:
|
|
creds, path = path.split('@')
|
|
else:
|
|
creds = None
|
|
if creds:
|
|
try:
|
|
self.user, self.password = creds.split(':')
|
|
except ValueError:
|
|
reason = _("Credentials are not well-formatted.")
|
|
LOG.info(reason)
|
|
raise exceptions.BadStoreUri(message=reason)
|
|
else:
|
|
self.user = None
|
|
if netloc == '':
|
|
LOG.info(_("No address specified in HTTP URL"))
|
|
raise exceptions.BadStoreUri(uri=uri)
|
|
else:
|
|
# IPv6 address has the following format [1223:0:0:..]:<some_port>
|
|
# we need to be sure that we are validating port in both IPv4,IPv6
|
|
delimiter = "]:" if netloc.count(":") > 1 else ":"
|
|
host, dlm, port = netloc.partition(delimiter)
|
|
# if port is present in location then validate port format
|
|
if port and not port.isdigit():
|
|
raise exceptions.BadStoreUri(uri=uri)
|
|
|
|
self.netloc = netloc
|
|
self.path = path
|
|
|
|
|
|
def http_response_iterator(conn, response, size):
|
|
"""
|
|
Return an iterator for a file-like object.
|
|
|
|
:param conn: HTTP(S) Connection
|
|
:param response: http_client.HTTPResponse object
|
|
:param size: Chunk size to iterate with
|
|
"""
|
|
chunk = response.read(size)
|
|
while chunk:
|
|
yield chunk
|
|
chunk = response.read(size)
|
|
conn.close()
|
|
|
|
|
|
class Store(glance_store.driver.Store):
|
|
|
|
"""An implementation of the HTTP(S) Backend Adapter"""
|
|
|
|
_CAPABILITIES = (capabilities.BitMasks.READ_ACCESS |
|
|
capabilities.BitMasks.DRIVER_REUSABLE)
|
|
|
|
@capabilities.check
|
|
def get(self, location, offset=0, chunk_size=None, context=None):
|
|
"""
|
|
Takes a `glance_store.location.Location` object that indicates
|
|
where to find the image file, and returns a tuple of generator
|
|
(for reading the image file) and image_size
|
|
|
|
:param location `glance_store.location.Location` object, supplied
|
|
from glance_store.location.get_location_from_uri()
|
|
"""
|
|
try:
|
|
conn, resp, content_length = self._query(location, 'GET')
|
|
except socket.error:
|
|
reason = _LE("Remote server where the image is present "
|
|
"is unavailable.")
|
|
LOG.error(reason)
|
|
raise exceptions.RemoteServiceUnavailable()
|
|
|
|
iterator = http_response_iterator(conn, resp, self.READ_CHUNKSIZE)
|
|
|
|
class ResponseIndexable(glance_store.Indexable):
|
|
def another(self):
|
|
try:
|
|
return next(self.wrapped)
|
|
except StopIteration:
|
|
return ''
|
|
|
|
return (ResponseIndexable(iterator, content_length), content_length)
|
|
|
|
def get_schemes(self):
|
|
return ('http', 'https')
|
|
|
|
def get_size(self, location, context=None):
|
|
"""
|
|
Takes a `glance_store.location.Location` object that indicates
|
|
where to find the image file, and returns the size
|
|
|
|
:param location `glance_store.location.Location` object, supplied
|
|
from glance_store.location.get_location_from_uri()
|
|
"""
|
|
try:
|
|
size = self._query(location, 'HEAD')[2]
|
|
except socket.error:
|
|
reason = _("The HTTP URL is invalid.")
|
|
LOG.info(reason)
|
|
raise exceptions.BadStoreUri(message=reason)
|
|
except exceptions.NotFound:
|
|
raise
|
|
except Exception:
|
|
# NOTE(flaper87): Catch more granular exceptions,
|
|
# keeping this branch for backwards compatibility.
|
|
return 0
|
|
return size
|
|
|
|
def _query(self, location, verb, depth=0):
|
|
if depth > MAX_REDIRECTS:
|
|
reason = (_("The HTTP URL exceeded %s maximum "
|
|
"redirects.") % MAX_REDIRECTS)
|
|
LOG.debug(reason)
|
|
raise exceptions.MaxRedirectsExceeded(message=reason)
|
|
loc = location.store_location
|
|
conn_class = self._get_conn_class(loc)
|
|
conn = conn_class(loc.netloc)
|
|
conn.request(verb, loc.path, "", {})
|
|
resp = conn.getresponse()
|
|
|
|
# Check for bad status codes
|
|
if resp.status >= 400:
|
|
if resp.status == http_client.NOT_FOUND:
|
|
reason = _("HTTP datastore could not find image at URI.")
|
|
LOG.debug(reason)
|
|
raise exceptions.NotFound(message=reason)
|
|
|
|
reason = (_("HTTP URL %(url)s returned a "
|
|
"%(status)s status code.") %
|
|
dict(url=loc.path, status=resp.status))
|
|
LOG.debug(reason)
|
|
raise exceptions.BadStoreUri(message=reason)
|
|
|
|
location_header = resp.getheader("location")
|
|
if location_header:
|
|
if resp.status not in (301, 302):
|
|
reason = (_("The HTTP URL %(url)s attempted to redirect "
|
|
"with an invalid %(status)s status code.") %
|
|
dict(url=loc.path, status=resp.status))
|
|
LOG.info(reason)
|
|
raise exceptions.BadStoreUri(message=reason)
|
|
location_class = glance_store.location.Location
|
|
new_loc = location_class(location.store_name,
|
|
location.store_location.__class__,
|
|
self.conf,
|
|
uri=location_header,
|
|
image_id=location.image_id,
|
|
store_specs=location.store_specs)
|
|
return self._query(new_loc, verb, depth + 1)
|
|
content_length = int(resp.getheader('content-length', 0))
|
|
return (conn, resp, content_length)
|
|
|
|
def _get_conn_class(self, loc):
|
|
"""
|
|
Returns connection class for accessing the resource. Useful
|
|
for dependency injection and stubouts in testing...
|
|
"""
|
|
return {'http': http_client.HTTPConnection,
|
|
'https': http_client.HTTPSConnection}[loc.scheme]
|