Merge "Add credential quoting to Swift's StoreLocation."

This commit is contained in:
Jenkins 2012-05-18 19:53:29 +00:00 committed by Gerrit Code Review
commit 5ffdc74a8d
5 changed files with 251 additions and 29 deletions

View File

@ -0,0 +1,163 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2012 OpenStack LLC.
# 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 types
import urlparse
import sqlalchemy
from glance.common import exception
import glance.store.swift
def upgrade(migrate_engine):
migrate_location_credentials(migrate_engine, to_quoted=True)
def downgrade(migrate_engine):
migrate_location_credentials(migrate_engine, to_quoted=False)
def migrate_location_credentials(migrate_engine, to_quoted):
"""
Migrate location credentials for swift uri's between the quoted
and unquoted forms.
:param migrate_engine: The configured db engine
:param to_quoted: If True, migrate location credentials from
unquoted to quoted form. If False, do the
reverse.
"""
meta = sqlalchemy.schema.MetaData()
meta.bind = migrate_engine
images_table = sqlalchemy.Table('images', meta, autoload=True)
images = images_table.select(images_table.c.location.startswith('swift')).\
execute()
for image in images:
fixed_uri = fix_uri_credentials(image['location'], to_quoted)
images_table.update().\
where(images_table.c.id == image['id']).\
values(location=fixed_uri).execute()
def fix_uri_credentials(uri, to_quoted):
"""
Fix the given uri's embedded credentials by round-tripping with
StoreLocation.
If to_quoted is True, the uri is assumed to have credentials that
have not been quoted, and the resulting uri will contain quoted
credentials.
If to_quoted is False, the uri is assumed to have credentials that
have been quoted, and the resulting uri will contain credentials
that have not been quoted.
"""
location = glance.store.swift.StoreLocation({})
if to_quoted:
# The legacy parse_uri doesn't unquote credentials
location.parse_uri = types.MethodType(legacy_parse_uri, location)
else:
# The legacy _get_credstring doesn't quote credentials
location._get_credstring = types.MethodType(legacy__get_credstring,
location)
location.parse_uri(uri)
return location.get_uri()
def legacy__get_credstring(self):
if self.user:
return '%s:%s@' % (self.user, self.key)
return ''
def legacy_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. It also deals with the peculiarity that new-style
Swift URIs have where a username can contain a ':', like so:
swift://account:user:pass@authurl.com/container/obj
"""
# Make sure that URIs that contain multiple schemes, such as:
# swift://user:pass@http://authurl.com/v1/container/obj
# are immediately rejected.
if uri.count('://') != 1:
reason = _(
"URI cannot contain more than one occurrence of a scheme."
"If you have specified a URI like "
"swift://user:pass@http://authurl.com/v1/container/obj"
", you need to change it to use the swift+http:// scheme, "
"like so: "
"swift+http://user:pass@authurl.com/v1/container/obj"
)
raise exception.BadStoreUri(uri, reason)
pieces = urlparse.urlparse(uri)
assert pieces.scheme in ('swift', 'swift+http', 'swift+https')
self.scheme = pieces.scheme
netloc = pieces.netloc
path = pieces.path.lstrip('/')
if netloc != '':
# > Python 2.6.1
if '@' in netloc:
creds, netloc = netloc.split('@')
else:
creds = None
else:
# Python 2.6.1 compat
# see lp659445 and Python issue7904
if '@' in path:
creds, path = path.split('@')
else:
creds = None
netloc = path[0:path.find('/')].strip('/')
path = path[path.find('/'):].strip('/')
if creds:
cred_parts = creds.split(':')
# User can be account:user, in which case cred_parts[0:2] will be
# the account and user. Combine them into a single username of
# account:user
if len(cred_parts) == 1:
reason = (_("Badly formed credentials '%(creds)s' in Swift "
"URI") % locals())
raise exception.BadStoreUri(uri, reason)
elif len(cred_parts) == 3:
user = ':'.join(cred_parts[0:2])
else:
user = cred_parts[0]
key = cred_parts[-1]
self.user = user
self.key = key
else:
self.user = None
path_parts = path.split('/')
try:
self.obj = path_parts.pop()
self.container = path_parts.pop()
if not netloc.startswith('http'):
# push hostname back into the remaining to build full authurl
path_parts.insert(0, netloc)
self.authurl = '/'.join(path_parts)
except IndexError:
reason = _("Badly formed Swift URI")
raise exception.BadStoreUri(uri, reason)

View File

@ -23,6 +23,7 @@ import hashlib
import httplib import httplib
import logging import logging
import math import math
import urllib
import urlparse import urlparse
from glance.common import exception from glance.common import exception
@ -70,7 +71,7 @@ class StoreLocation(glance.store.location.StoreLocation):
def _get_credstring(self): def _get_credstring(self):
if self.user: if self.user:
return '%s:%s@' % (self.user, self.key) return '%s:%s@' % (urllib.quote(self.user), urllib.quote(self.key))
return '' return ''
def get_uri(self): def get_uri(self):
@ -133,21 +134,13 @@ class StoreLocation(glance.store.location.StoreLocation):
path = path[path.find('/'):].strip('/') path = path[path.find('/'):].strip('/')
if creds: if creds:
cred_parts = creds.split(':') cred_parts = creds.split(':')
if len(cred_parts) != 2:
# User can be account:user, in which case cred_parts[0:2] will be
# the account and user. Combine them into a single username of
# account:user
if len(cred_parts) == 1:
reason = (_("Badly formed credentials '%(creds)s' in Swift " reason = (_("Badly formed credentials '%(creds)s' in Swift "
"URI") % locals()) "URI") % locals())
raise exception.BadStoreUri(uri, reason) raise exception.BadStoreUri(uri, reason)
elif len(cred_parts) == 3: user, key = cred_parts
user = ':'.join(cred_parts[0:2]) self.user = urllib.unquote(user)
else: self.key = urllib.unquote(key)
user = cred_parts[0]
key = cred_parts[-1]
self.user = user
self.key = key
else: else:
self.user = None self.user = None
path_parts = path.split('/') path_parts = path.split('/')

View File

@ -316,3 +316,63 @@ class TestMigrations(unittest.TestCase):
last_num_image_properties = conn.execute(sel).scalar() last_num_image_properties = conn.execute(sel).scalar()
self.assertEqual(num_image_properties - 2, last_num_image_properties) self.assertEqual(num_image_properties - 2, last_num_image_properties)
def test_no_data_loss_14_to_15(self):
for key, engine in self.engines.items():
conf = utils.TestConfigOpts({
'sql_connection': TestMigrations.TEST_DATABASES[key]})
conf.register_opt(cfg.StrOpt('sql_connection'))
self._check_no_data_loss_14_to_15(engine, conf)
def _check_no_data_loss_14_to_15(self, engine, conf):
"""
Check that migrating swift location credentials to quoted form
and back does not result in data loss.
"""
migration_api.version_control(conf, version=0)
migration_api.upgrade(conf, 14)
conn = engine.connect()
images_table = Table('images', MetaData(), autoload=True,
autoload_with=engine)
def get_locations():
conn = engine.connect()
locations = [x[0] for x in
conn.execute(
select(['location'], from_obj=[images_table]))]
conn.close()
return locations
unquoted_locations = [
'swift://acct:usr:pass@example.com/container/obj-id',
'file://foo',
]
quoted_locations = [
'swift://acct%3Ausr:pass@example.com/container/obj-id',
'file://foo',
]
# Insert images with an unquoted image location
now = datetime.datetime.now()
kwargs = dict(
deleted=False,
created_at=now,
updated_at=now,
status='active',
is_public=True,
min_disk=0,
min_ram=0,
)
for i, location in enumerate(unquoted_locations):
kwargs.update(location=location, id=i)
conn.execute(images_table.insert(), [kwargs])
conn.close()
migration_api.upgrade(conf, 15)
self.assertEqual(get_locations(), quoted_locations)
migration_api.downgrade(conf, 14)
self.assertEqual(get_locations(), unquoted_locations)

View File

@ -46,8 +46,8 @@ class TestStoreLocation(base.StoreClearingUnitTest):
good_store_uris = [ good_store_uris = [
'https://user:pass@example.com:80/images/some-id', 'https://user:pass@example.com:80/images/some-id',
'http://images.oracle.com/123456', 'http://images.oracle.com/123456',
'swift://account:user:pass@authurl.com/container/obj-id', 'swift://account%3Auser:pass@authurl.com/container/obj-id',
'swift+https://account:user:pass@authurl.com/container/obj-id', 'swift+https://account%3Auser:pass@authurl.com/container/obj-id',
's3://accesskey:secretkey@s3.amazonaws.com/bucket/key-id', 's3://accesskey:secretkey@s3.amazonaws.com/bucket/key-id',
's3://accesskey:secretwith/aslash@s3.amazonaws.com/bucket/key-id', 's3://accesskey:secretwith/aslash@s3.amazonaws.com/bucket/key-id',
's3+http://accesskey:secret@s3.amazonaws.com/bucket/key-id', 's3+http://accesskey:secret@s3.amazonaws.com/bucket/key-id',
@ -178,7 +178,8 @@ class TestStoreLocation(base.StoreClearingUnitTest):
self.assertEqual("pass", loc.key) self.assertEqual("pass", loc.key)
self.assertEqual(uri, loc.get_uri()) self.assertEqual(uri, loc.get_uri())
uri = 'swift+http://account:user:pass@authurl.com/v1/container/12345' uri = ('swift+http://a%3Auser%40example.com:p%40ss@authurl.com/'
'v1/container/12345')
loc.parse_uri(uri) loc.parse_uri(uri)
self.assertEqual("swift+http", loc.scheme) self.assertEqual("swift+http", loc.scheme)
@ -186,8 +187,8 @@ class TestStoreLocation(base.StoreClearingUnitTest):
self.assertEqual("http://authurl.com/v1", loc.swift_auth_url) self.assertEqual("http://authurl.com/v1", loc.swift_auth_url)
self.assertEqual("container", loc.container) self.assertEqual("container", loc.container)
self.assertEqual("12345", loc.obj) self.assertEqual("12345", loc.obj)
self.assertEqual("account:user", loc.user) self.assertEqual("a:user@example.com", loc.user)
self.assertEqual("pass", loc.key) self.assertEqual("p@ss", loc.key)
self.assertEqual(uri, loc.get_uri()) self.assertEqual(uri, loc.get_uri())
bad_uri = 'swif://' bad_uri = 'swif://'

View File

@ -22,6 +22,7 @@ import hashlib
import httplib import httplib
import tempfile import tempfile
import unittest import unittest
import urllib
import stubout import stubout
import swift.common.client import swift.common.client
@ -190,12 +191,16 @@ def stub_out_swift_common_client(stubs, conf):
class SwiftTests(object): class SwiftTests(object):
@property
def swift_store_user(self):
return urllib.quote(self.conf['swift_store_user'])
def test_get_size(self): def test_get_size(self):
""" """
Test that we can get the size of an object in the swift store Test that we can get the size of an object in the swift store
""" """
uri = "swift://%s:key@auth_address/glance/%s" % ( uri = "swift://%s:key@auth_address/glance/%s" % (
self.conf['swift_store_user'], FAKE_UUID) self.swift_store_user, FAKE_UUID)
loc = get_location_from_uri(uri) loc = get_location_from_uri(uri)
image_size = self.store.get_size(loc) image_size = self.store.get_size(loc)
self.assertEqual(image_size, 5120) self.assertEqual(image_size, 5120)
@ -203,7 +208,7 @@ class SwiftTests(object):
def test_get(self): def test_get(self):
"""Test a "normal" retrieval of an image in chunks""" """Test a "normal" retrieval of an image in chunks"""
uri = "swift://%s:key@auth_address/glance/%s" % ( uri = "swift://%s:key@auth_address/glance/%s" % (
self.conf['swift_store_user'], FAKE_UUID) self.swift_store_user, FAKE_UUID)
loc = get_location_from_uri(uri) loc = get_location_from_uri(uri)
(image_swift, image_size) = self.store.get(loc) (image_swift, image_size) = self.store.get(loc)
self.assertEqual(image_size, 5120) self.assertEqual(image_size, 5120)
@ -223,7 +228,7 @@ class SwiftTests(object):
""" """
loc = get_location_from_uri("swift+http://%s:key@auth_address/" loc = get_location_from_uri("swift+http://%s:key@auth_address/"
"glance/%s" % ( "glance/%s" % (
self.conf['swift_store_user'], FAKE_UUID)) self.swift_store_user, FAKE_UUID))
(image_swift, image_size) = self.store.get(loc) (image_swift, image_size) = self.store.get(loc)
self.assertEqual(image_size, 5120) self.assertEqual(image_size, 5120)
@ -240,7 +245,7 @@ class SwiftTests(object):
raises an error raises an error
""" """
loc = get_location_from_uri("swift://%s:key@authurl/glance/noexist" % ( loc = get_location_from_uri("swift://%s:key@authurl/glance/noexist" % (
self.conf['swift_store_user'])) self.swift_store_user))
self.assertRaises(exception.NotFound, self.assertRaises(exception.NotFound,
self.store.get, self.store.get,
loc) loc)
@ -252,7 +257,7 @@ class SwiftTests(object):
expected_checksum = hashlib.md5(expected_swift_contents).hexdigest() expected_checksum = hashlib.md5(expected_swift_contents).hexdigest()
expected_image_id = utils.generate_uuid() expected_image_id = utils.generate_uuid()
loc = 'swift+https://%s:key@localhost:8080/glance/%s' loc = 'swift+https://%s:key@localhost:8080/glance/%s'
expected_location = loc % (self.conf['swift_store_user'], expected_location = loc % (self.swift_store_user,
expected_image_id) expected_image_id)
image_swift = StringIO.StringIO(expected_swift_contents) image_swift = StringIO.StringIO(expected_swift_contents)
@ -305,7 +310,7 @@ class SwiftTests(object):
for variation, expected_location in variations.items(): for variation, expected_location in variations.items():
image_id = utils.generate_uuid() image_id = utils.generate_uuid()
expected_location = expected_location % ( expected_location = expected_location % (
self.conf['swift_store_user'], image_id) self.swift_store_user, image_id)
expected_swift_size = FIVE_KB expected_swift_size = FIVE_KB
expected_swift_contents = "*" * expected_swift_size expected_swift_contents = "*" * expected_swift_size
expected_checksum = \ expected_checksum = \
@ -372,7 +377,7 @@ class SwiftTests(object):
expected_checksum = hashlib.md5(expected_swift_contents).hexdigest() expected_checksum = hashlib.md5(expected_swift_contents).hexdigest()
expected_image_id = utils.generate_uuid() expected_image_id = utils.generate_uuid()
loc = 'swift+https://%s:key@localhost:8080/noexist/%s' loc = 'swift+https://%s:key@localhost:8080/noexist/%s'
expected_location = loc % (self.conf['swift_store_user'], expected_location = loc % (self.swift_store_user,
expected_image_id) expected_image_id)
image_swift = StringIO.StringIO(expected_swift_contents) image_swift = StringIO.StringIO(expected_swift_contents)
@ -410,7 +415,7 @@ class SwiftTests(object):
expected_checksum = hashlib.md5(expected_swift_contents).hexdigest() expected_checksum = hashlib.md5(expected_swift_contents).hexdigest()
expected_image_id = utils.generate_uuid() expected_image_id = utils.generate_uuid()
loc = 'swift+https://%s:key@localhost:8080/glance/%s' loc = 'swift+https://%s:key@localhost:8080/glance/%s'
expected_location = loc % (self.conf['swift_store_user'], expected_location = loc % (self.swift_store_user,
expected_image_id) expected_image_id)
image_swift = StringIO.StringIO(expected_swift_contents) image_swift = StringIO.StringIO(expected_swift_contents)
@ -464,7 +469,7 @@ class SwiftTests(object):
expected_checksum = hashlib.md5(expected_swift_contents).hexdigest() expected_checksum = hashlib.md5(expected_swift_contents).hexdigest()
expected_image_id = utils.generate_uuid() expected_image_id = utils.generate_uuid()
loc = 'swift+https://%s:key@localhost:8080/glance/%s' loc = 'swift+https://%s:key@localhost:8080/glance/%s'
expected_location = loc % (self.conf['swift_store_user'], expected_location = loc % (self.swift_store_user,
expected_image_id) expected_image_id)
image_swift = StringIO.StringIO(expected_swift_contents) image_swift = StringIO.StringIO(expected_swift_contents)
@ -549,7 +554,7 @@ class SwiftTests(object):
Test we can delete an existing image in the swift store Test we can delete an existing image in the swift store
""" """
uri = "swift://%s:key@authurl/glance/%s" % ( uri = "swift://%s:key@authurl/glance/%s" % (
self.conf['swift_store_user'], FAKE_UUID) self.swift_store_user, FAKE_UUID)
loc = get_location_from_uri(uri) loc = get_location_from_uri(uri)
self.store.delete(loc) self.store.delete(loc)
@ -561,7 +566,7 @@ class SwiftTests(object):
raises an error raises an error
""" """
loc = get_location_from_uri("swift://%s:key@authurl/glance/noexist" % ( loc = get_location_from_uri("swift://%s:key@authurl/glance/noexist" % (
self.conf['swift_store_user'])) self.swift_store_user))
self.assertRaises(exception.NotFound, self.store.delete, loc) self.assertRaises(exception.NotFound, self.store.delete, loc)