Adds ability for Swift to be used as a full-fledged backend.
Adds POST/PUT capabilities to the SwiftBackend Adds lots of unit tests for both FilesystemBackend and SwiftBackend Removes now-unused tests.unit.fakeswifthttp module
This commit is contained in:
parent
ed1b5758e0
commit
2cf64655da
@ -8,10 +8,6 @@ debug = False
|
|||||||
[app:glance-api]
|
[app:glance-api]
|
||||||
paste.app_factory = glance.server:app_factory
|
paste.app_factory = glance.server:app_factory
|
||||||
|
|
||||||
# Directory that the Filesystem backend store
|
|
||||||
# writes image data to
|
|
||||||
filesystem_store_datadir=/var/lib/glance/images/
|
|
||||||
|
|
||||||
# Which backend store should Glance use by default is not specified
|
# Which backend store should Glance use by default is not specified
|
||||||
# in a request to add a new image to Glance? Default: 'file'
|
# in a request to add a new image to Glance? Default: 'file'
|
||||||
# Available choices are 'file', 'swift', and 's3'
|
# Available choices are 'file', 'swift', and 's3'
|
||||||
@ -29,6 +25,32 @@ registry_host = 0.0.0.0
|
|||||||
# Port the registry server is listening on
|
# Port the registry server is listening on
|
||||||
registry_port = 9191
|
registry_port = 9191
|
||||||
|
|
||||||
|
# ============ Filesystem Store Options ========================
|
||||||
|
|
||||||
|
# Directory that the Filesystem backend store
|
||||||
|
# writes image data to
|
||||||
|
filesystem_store_datadir=/var/lib/glance/images/
|
||||||
|
|
||||||
|
# ============ Swift Store Options =============================
|
||||||
|
|
||||||
|
# Address where the Swift authentication service lives
|
||||||
|
swift_store_auth_address = 127.0.0.1:8080
|
||||||
|
|
||||||
|
# User to authenticate against the Swift authentication service
|
||||||
|
swift_store_user = jdoe
|
||||||
|
|
||||||
|
# Auth key for the user authenticating against the
|
||||||
|
# Swift authentication service
|
||||||
|
swift_store_key = a86850deb2742ec3cb41518e26aa2d89
|
||||||
|
|
||||||
|
# Account to use for the user:key Swift auth combination
|
||||||
|
# for storing images in Swift
|
||||||
|
swift_store_account = glance
|
||||||
|
|
||||||
|
# Container within the account that the account should use
|
||||||
|
# for storing images in Swift
|
||||||
|
swift_store_container = glance
|
||||||
|
|
||||||
[app:glance-registry]
|
[app:glance-registry]
|
||||||
paste.app_factory = glance.registry.server:app_factory
|
paste.app_factory = glance.registry.server:app_factory
|
||||||
|
|
||||||
|
@ -154,7 +154,8 @@ class Controller(wsgi.Controller):
|
|||||||
|
|
||||||
def image_iterator():
|
def image_iterator():
|
||||||
chunks = get_from_backend(image['location'],
|
chunks = get_from_backend(image['location'],
|
||||||
expected_size=image['size'])
|
expected_size=image['size'],
|
||||||
|
options=self.options)
|
||||||
|
|
||||||
for chunk in chunks:
|
for chunk in chunks:
|
||||||
yield chunk
|
yield chunk
|
||||||
|
@ -19,12 +19,15 @@
|
|||||||
A simple filesystem-backed store
|
A simple filesystem-backed store
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
import os
|
import os
|
||||||
import urlparse
|
import urlparse
|
||||||
|
|
||||||
from glance.common import exception
|
from glance.common import exception
|
||||||
import glance.store
|
import glance.store
|
||||||
|
|
||||||
|
logger = logging.getLogger('glance.store.filesystem')
|
||||||
|
|
||||||
|
|
||||||
class ChunkedFile(object):
|
class ChunkedFile(object):
|
||||||
|
|
||||||
@ -60,8 +63,7 @@ class ChunkedFile(object):
|
|||||||
|
|
||||||
class FilesystemBackend(glance.store.Backend):
|
class FilesystemBackend(glance.store.Backend):
|
||||||
@classmethod
|
@classmethod
|
||||||
def get(cls, parsed_uri, opener=lambda p: open(p, "rb"),
|
def get(cls, parsed_uri, expected_size=None, options=None):
|
||||||
expected_size=None):
|
|
||||||
""" Filesystem-based backend
|
""" Filesystem-based backend
|
||||||
|
|
||||||
file:///path/to/file.tar.gz.0
|
file:///path/to/file.tar.gz.0
|
||||||
@ -71,6 +73,8 @@ class FilesystemBackend(glance.store.Backend):
|
|||||||
if not os.path.exists(filepath):
|
if not os.path.exists(filepath):
|
||||||
raise exception.NotFound("Image file %s not found" % filepath)
|
raise exception.NotFound("Image file %s not found" % filepath)
|
||||||
else:
|
else:
|
||||||
|
logger.debug("Found image at %s. Returning in ChunkedFile.",
|
||||||
|
filepath)
|
||||||
return ChunkedFile(filepath)
|
return ChunkedFile(filepath)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@ -87,6 +91,7 @@ class FilesystemBackend(glance.store.Backend):
|
|||||||
fn = parsed_uri.path
|
fn = parsed_uri.path
|
||||||
if os.path.exists(fn):
|
if os.path.exists(fn):
|
||||||
try:
|
try:
|
||||||
|
logger.debug("Deleting image at %s", fn)
|
||||||
os.unlink(fn)
|
os.unlink(fn)
|
||||||
except OSError:
|
except OSError:
|
||||||
raise exception.NotAuthorized("You cannot delete file %s" % fn)
|
raise exception.NotAuthorized("You cannot delete file %s" % fn)
|
||||||
@ -112,6 +117,8 @@ class FilesystemBackend(glance.store.Backend):
|
|||||||
datadir = options['filesystem_store_datadir']
|
datadir = options['filesystem_store_datadir']
|
||||||
|
|
||||||
if not os.path.exists(datadir):
|
if not os.path.exists(datadir):
|
||||||
|
logger.info("Directory to write image files does not exist "
|
||||||
|
"(%s). Creating.", datadir)
|
||||||
os.makedirs(datadir)
|
os.makedirs(datadir)
|
||||||
|
|
||||||
filepath = os.path.join(datadir, str(id))
|
filepath = os.path.join(datadir, str(id))
|
||||||
@ -129,6 +136,8 @@ class FilesystemBackend(glance.store.Backend):
|
|||||||
bytes_written += len(buf)
|
bytes_written += len(buf)
|
||||||
f.write(buf)
|
f.write(buf)
|
||||||
|
|
||||||
|
logger.debug("Wrote %(bytes_written)d bytes to %(filepath)s"
|
||||||
|
% locals())
|
||||||
return ('file://%s' % filepath, bytes_written)
|
return ('file://%s' % filepath, bytes_written)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||||
|
|
||||||
# Copyright 2010 OpenStack, LLC
|
# Copyright 2010-2011 OpenStack, LLC
|
||||||
# All Rights Reserved.
|
# All Rights Reserved.
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
@ -15,20 +15,33 @@
|
|||||||
# 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 __future__ import absolute_import
|
"""Storage backend for SWIFT"""
|
||||||
|
|
||||||
|
import httplib
|
||||||
|
import logging
|
||||||
|
import urllib
|
||||||
|
|
||||||
|
from glance.common import config
|
||||||
|
from glance.common import exception
|
||||||
import glance.store
|
import glance.store
|
||||||
|
|
||||||
|
DEFAULT_SWIFT_ACCOUNT = 'glance'
|
||||||
|
DEFAULT_SWIFT_CONTAINER = 'glance'
|
||||||
|
|
||||||
|
logger = logging.getLogger('glance.store.swift')
|
||||||
|
|
||||||
|
|
||||||
class SwiftBackend(glance.store.Backend):
|
class SwiftBackend(glance.store.Backend):
|
||||||
"""
|
"""
|
||||||
An implementation of the swift backend adapter.
|
An implementation of the swift backend adapter.
|
||||||
"""
|
"""
|
||||||
EXAMPLE_URL = "swift://user:password@auth_url/container/file.gz.0"
|
EXAMPLE_URL = "swift://<USER>:<KEY>@<AUTH_ADDRESS>/<CONTAINER>/<FILE>"
|
||||||
|
|
||||||
CHUNKSIZE = 65536
|
CHUNKSIZE = 65536
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get(cls, parsed_uri, expected_size, conn_class=None):
|
def get(cls, parsed_uri, expected_size=None, options=None,
|
||||||
|
conn_class=None):
|
||||||
"""
|
"""
|
||||||
Takes a parsed_uri in the format of:
|
Takes a parsed_uri in the format of:
|
||||||
swift://user:password@auth_url/container/file.gz.0, connects to the
|
swift://user:password@auth_url/container/file.gz.0, connects to the
|
||||||
@ -43,12 +56,23 @@ class SwiftBackend(glance.store.Backend):
|
|||||||
# snet=True
|
# snet=True
|
||||||
connection_class = get_connection_class(conn_class)
|
connection_class = get_connection_class(conn_class)
|
||||||
|
|
||||||
swift_conn = conn_class(
|
swift_conn = connection_class(
|
||||||
authurl=authurl, user=user, key=key, snet=False)
|
authurl=authurl, user=user, key=key, snet=False)
|
||||||
|
|
||||||
|
try:
|
||||||
(resp_headers, resp_body) = swift_conn.get_object(
|
(resp_headers, resp_body) = swift_conn.get_object(
|
||||||
container=container, obj=obj, resp_chunk_size=cls.CHUNKSIZE)
|
container=container, obj=obj, resp_chunk_size=cls.CHUNKSIZE)
|
||||||
|
|
||||||
|
# TODO(jaypipes) use real exceptions when remove all the cruft
|
||||||
|
# around importing Swift stuff...
|
||||||
|
except Exception, e:
|
||||||
|
if e.http_status == httplib.NOT_FOUND:
|
||||||
|
location = "swift://%s:%s@%s/%s/%s" % (user, key, authurl,
|
||||||
|
container, obj)
|
||||||
|
raise exception.NotFound("Swift could not find image at "
|
||||||
|
"location %(location)s" % locals())
|
||||||
|
|
||||||
|
if expected_size:
|
||||||
obj_size = int(resp_headers['content-length'])
|
obj_size = int(resp_headers['content-length'])
|
||||||
if obj_size != expected_size:
|
if obj_size != expected_size:
|
||||||
raise glance.store.BackendException(
|
raise glance.store.BackendException(
|
||||||
@ -57,6 +81,105 @@ class SwiftBackend(glance.store.Backend):
|
|||||||
|
|
||||||
return resp_body
|
return resp_body
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def add(cls, id, data, options, conn_class=None):
|
||||||
|
"""
|
||||||
|
Stores image data to Swift and returns a location that the image was
|
||||||
|
written to.
|
||||||
|
|
||||||
|
Swift writes the image data using the scheme:
|
||||||
|
``swift://<USER>:<KEY>@<AUTH_ADDRESS>/<CONTAINER>/<ID>`
|
||||||
|
where:
|
||||||
|
<USER> = ``swift_store_user``
|
||||||
|
<KEY> = ``swift_store_key``
|
||||||
|
<AUTH_ADDRESS> = ``swift_store_auth_address``
|
||||||
|
<CONTAINER> = ``swift_store_container``
|
||||||
|
<ID> = The id of the image being added
|
||||||
|
|
||||||
|
:param id: The opaque image identifier
|
||||||
|
:param data: The image data to write, as a file-like object
|
||||||
|
:param options: Conf mapping
|
||||||
|
|
||||||
|
:retval Tuple with (location, size)
|
||||||
|
The location that was written,
|
||||||
|
and the size in bytes of the data written
|
||||||
|
"""
|
||||||
|
account = options.get('swift_store_account',
|
||||||
|
DEFAULT_SWIFT_ACCOUNT)
|
||||||
|
container = options.get('swift_store_container',
|
||||||
|
DEFAULT_SWIFT_CONTAINER)
|
||||||
|
auth_address = options.get('swift_store_auth_address')
|
||||||
|
user = options.get('swift_store_user')
|
||||||
|
key = options.get('swift_store_key')
|
||||||
|
|
||||||
|
# TODO(jaypipes): This needs to be checked every time
|
||||||
|
# because of the decision to make glance.store.Backend's
|
||||||
|
# interface all @classmethods. This is inefficient. Backend
|
||||||
|
# should be a stateful object with options parsed once in
|
||||||
|
# a constructor.
|
||||||
|
if not auth_address:
|
||||||
|
logger.error(msg)
|
||||||
|
msg = ("Could not find swift_store_auth_address in configuration "
|
||||||
|
"options.")
|
||||||
|
raise glance.store.BackendException(msg)
|
||||||
|
else:
|
||||||
|
full_auth_address = auth_address
|
||||||
|
if not full_auth_address.startswith('http'):
|
||||||
|
full_auth_address = 'https://' + full_auth_address
|
||||||
|
|
||||||
|
if not user:
|
||||||
|
logger.error(msg)
|
||||||
|
msg = ("Could not find swift_store_user in configuration "
|
||||||
|
"options.")
|
||||||
|
raise glance.store.BackendException(msg)
|
||||||
|
|
||||||
|
if not key:
|
||||||
|
logger.error(msg)
|
||||||
|
msg = ("Could not find swift_store_key in configuration "
|
||||||
|
"options.")
|
||||||
|
raise glance.store.BackendException(msg)
|
||||||
|
|
||||||
|
connection_class = get_connection_class(conn_class)
|
||||||
|
swift_conn = connection_class(authurl=full_auth_address, user=user,
|
||||||
|
key=key, snet=False)
|
||||||
|
|
||||||
|
logger.debug("Adding image object to Swift using "
|
||||||
|
"(auth_address=%(auth_address)s, user=%(user)s, "
|
||||||
|
"key=%(key)s)")
|
||||||
|
|
||||||
|
try:
|
||||||
|
obj_etag = swift_conn.put_object(container, id, data)
|
||||||
|
|
||||||
|
# NOTE: We return the user and key here! Have to because
|
||||||
|
# location is used by the API server to return the actual
|
||||||
|
# image data. We *really* should consider NOT returning
|
||||||
|
# the location attribute from GET /images/<ID> and
|
||||||
|
# GET /images/details
|
||||||
|
location = "swift://%(user)s:%(key)s@%(auth_address)s/"\
|
||||||
|
"%(container)s/%(id)s" % locals()
|
||||||
|
|
||||||
|
# We do a HEAD on the newly-added image to determine the size
|
||||||
|
# of the image. A bit slow, but better than taking the word
|
||||||
|
# of the user adding the image with size attribute in the metadata
|
||||||
|
resp_headers = swift_conn.head_object(container, id)
|
||||||
|
size = 0
|
||||||
|
# header keys are lowercased by Swift
|
||||||
|
if 'content-length' in resp_headers:
|
||||||
|
size = int(resp_headers['content-length'])
|
||||||
|
return (location, size)
|
||||||
|
|
||||||
|
# TODO(jaypipes) use real exceptions when remove all the cruft
|
||||||
|
# around importing Swift stuff...
|
||||||
|
except Exception, e:
|
||||||
|
if e.http_status == httplib.CONFLICT:
|
||||||
|
location = "swift://%s:%s@%s/%s/%s" % (user, key, auth_address,
|
||||||
|
container, id)
|
||||||
|
raise exception.Duplicate("Swift already has an image at "
|
||||||
|
"location %(location)s" % locals())
|
||||||
|
msg = ("Failed to add object to Swift.\n"
|
||||||
|
"Got error from Swift: %(e)s" % locals())
|
||||||
|
raise glance.store.BackendException(msg)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def delete(cls, parsed_uri, conn_class=None):
|
def delete(cls, parsed_uri, conn_class=None):
|
||||||
"""
|
"""
|
||||||
@ -70,14 +193,20 @@ class SwiftBackend(glance.store.Backend):
|
|||||||
# snet=True
|
# snet=True
|
||||||
connection_class = get_connection_class(conn_class)
|
connection_class = get_connection_class(conn_class)
|
||||||
|
|
||||||
swift_conn = conn_class(
|
swift_conn = connection_class(
|
||||||
authurl=authurl, user=user, key=key, snet=False)
|
authurl=authurl, user=user, key=key, snet=False)
|
||||||
|
|
||||||
(resp_headers, resp_body) = swift_conn.delete_object(
|
try:
|
||||||
container=container, obj=obj)
|
swift_conn.delete_object(container, obj)
|
||||||
|
|
||||||
# TODO(jaypipes): What to return here? After reading the docs
|
# TODO(jaypipes) use real exceptions when remove all the cruft
|
||||||
# at swift.common.client, I'm not sure what to check for...
|
# around importing Swift stuff...
|
||||||
|
except Exception, e:
|
||||||
|
if e.http_status == httplib.NOT_FOUND:
|
||||||
|
location = "swift://%s:%s@%s/%s/%s" % (user, key, authurl,
|
||||||
|
container, obj)
|
||||||
|
raise exception.NotFound("Swift could not find image at "
|
||||||
|
"location %(location)s" % locals())
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _parse_swift_tokens(cls, parsed_uri):
|
def _parse_swift_tokens(cls, parsed_uri):
|
||||||
|
@ -184,35 +184,6 @@ def stub_out_swift_backend(stubs):
|
|||||||
fake_swift_backend.get)
|
fake_swift_backend.get)
|
||||||
|
|
||||||
|
|
||||||
def stub_out_registry(stubs):
|
|
||||||
"""Stubs out the Registry registry with fake data returns.
|
|
||||||
|
|
||||||
The stubbed Registry always returns the following fixture::
|
|
||||||
|
|
||||||
{'files': [
|
|
||||||
{'location': 'file:///chunk0', 'size': 12345},
|
|
||||||
{'location': 'file:///chunk1', 'size': 1235}
|
|
||||||
]}
|
|
||||||
|
|
||||||
:param stubs: Set of stubout stubs
|
|
||||||
|
|
||||||
"""
|
|
||||||
class FakeRegistry(object):
|
|
||||||
|
|
||||||
DATA = \
|
|
||||||
{'files': [
|
|
||||||
{'location': 'file:///chunk0', 'size': 12345},
|
|
||||||
{'location': 'file:///chunk1', 'size': 1235}]}
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def lookup(cls, _parsed_uri):
|
|
||||||
return cls.DATA
|
|
||||||
|
|
||||||
fake_registry_registry = FakeRegistry()
|
|
||||||
stubs.Set(glance.store.registries.Registry, 'lookup',
|
|
||||||
fake_registry_registry.lookup)
|
|
||||||
|
|
||||||
|
|
||||||
def stub_out_registry_and_store_server(stubs):
|
def stub_out_registry_and_store_server(stubs):
|
||||||
"""
|
"""
|
||||||
Mocks calls to 127.0.0.1 on 9191 and 9292 for testing so
|
Mocks calls to 127.0.0.1 on 9191 and 9292 for testing so
|
||||||
|
@ -1,294 +0,0 @@
|
|||||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
|
||||||
|
|
||||||
# Copyright 2010 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.
|
|
||||||
|
|
||||||
"""
|
|
||||||
fakehttp/socket implementation
|
|
||||||
|
|
||||||
- TrackerSocket: an object which masquerades as a socket and responds to
|
|
||||||
requests in a manner consistent with a *very* stupid CloudFS tracker.
|
|
||||||
|
|
||||||
- CustomHTTPConnection: an object which subclasses httplib.HTTPConnection
|
|
||||||
in order to replace it's socket with a TrackerSocket instance.
|
|
||||||
|
|
||||||
The unittests each have setup methods which create freerange connection
|
|
||||||
instances that have had their HTTPConnection instances replaced by
|
|
||||||
intances of CustomHTTPConnection.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from httplib import HTTPConnection as connbase
|
|
||||||
import StringIO
|
|
||||||
|
|
||||||
|
|
||||||
class FakeSocket(object):
|
|
||||||
def __init__(self):
|
|
||||||
self._rbuffer = StringIO.StringIO()
|
|
||||||
self._wbuffer = StringIO.StringIO()
|
|
||||||
|
|
||||||
def close(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def send(self, data, flags=0):
|
|
||||||
self._rbuffer.write(data)
|
|
||||||
sendall = send
|
|
||||||
|
|
||||||
def recv(self, len=1024, flags=0):
|
|
||||||
return self._wbuffer(len)
|
|
||||||
|
|
||||||
def connect(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def makefile(self, mode, flags):
|
|
||||||
return self._wbuffer
|
|
||||||
|
|
||||||
|
|
||||||
class TrackerSocket(FakeSocket):
|
|
||||||
def write(self, data):
|
|
||||||
self._wbuffer.write(data)
|
|
||||||
|
|
||||||
def read(self, length=-1):
|
|
||||||
return self._rbuffer.read(length)
|
|
||||||
|
|
||||||
def _create_GET_account_content(self, path, args):
|
|
||||||
if 'format' in args and args['format'] == 'json':
|
|
||||||
containers = []
|
|
||||||
containers.append('[\n')
|
|
||||||
containers.append('{"name":"container1","count":2,"bytes":78},\n')
|
|
||||||
containers.append('{"name":"container2","count":1,"bytes":39},\n')
|
|
||||||
containers.append('{"name":"container3","count":3,"bytes":117}\n')
|
|
||||||
containers.append(']\n')
|
|
||||||
elif 'format' in args and args['format'] == 'xml':
|
|
||||||
containers = []
|
|
||||||
containers.append('<?xml version="1.0" encoding="UTF-8"?>\n')
|
|
||||||
containers.append('<account name="FakeAccount">\n')
|
|
||||||
containers.append('<container><name>container1</name>'
|
|
||||||
'<count>2</count>'
|
|
||||||
'<bytes>78</bytes></container>\n')
|
|
||||||
containers.append('<container><name>container2</name>'
|
|
||||||
'<count>1</count>'
|
|
||||||
'<bytes>39</bytes></container>\n')
|
|
||||||
containers.append('<container><name>container3</name>'
|
|
||||||
'<count>3</count>'
|
|
||||||
'<bytes>117</bytes></container>\n')
|
|
||||||
containers.append('</account>\n')
|
|
||||||
else:
|
|
||||||
containers = ['container%s\n' % i for i in range(1, 4)]
|
|
||||||
return ''.join(containers)
|
|
||||||
|
|
||||||
def _create_GET_container_content(self, path, args):
|
|
||||||
left = 0
|
|
||||||
right = 9
|
|
||||||
if 'offset' in args:
|
|
||||||
left = int(args['offset'])
|
|
||||||
if 'limit' in args:
|
|
||||||
right = left + int(args['limit'])
|
|
||||||
|
|
||||||
if 'format' in args and args['format'] == 'json':
|
|
||||||
objects = []
|
|
||||||
objects.append('{"name":"object1",'
|
|
||||||
'"hash":"4281c348eaf83e70ddce0e07221c3d28",'
|
|
||||||
'"bytes":14,'
|
|
||||||
'"content_type":"application\/octet-stream",'
|
|
||||||
'"last_modified":"2007-03-04 20:32:17"}')
|
|
||||||
objects.append('{"name":"object2",'
|
|
||||||
'"hash":"b039efe731ad111bc1b0ef221c3849d0",'
|
|
||||||
'"bytes":64,'
|
|
||||||
'"content_type":"application\/octet-stream",'
|
|
||||||
'"last_modified":"2007-03-04 20:32:17"}')
|
|
||||||
objects.append('{"name":"object3",'
|
|
||||||
'"hash":"4281c348eaf83e70ddce0e07221c3d28",'
|
|
||||||
'"bytes":14,'
|
|
||||||
'"content_type":"application\/octet-stream",'
|
|
||||||
'"last_modified":"2007-03-04 20:32:17"}')
|
|
||||||
objects.append('{"name":"object4",'
|
|
||||||
'"hash":"b039efe731ad111bc1b0ef221c3849d0",'
|
|
||||||
'"bytes":64,'
|
|
||||||
'"content_type":"application\/octet-stream",'
|
|
||||||
'"last_modified":"2007-03-04 20:32:17"}')
|
|
||||||
objects.append('{"name":"object5",'
|
|
||||||
'"hash":"4281c348eaf83e70ddce0e07221c3d28",'
|
|
||||||
'"bytes":14,'
|
|
||||||
'"content_type":"application\/octet-stream",'
|
|
||||||
'"last_modified":"2007-03-04 20:32:17"}')
|
|
||||||
objects.append('{"name":"object6",'
|
|
||||||
'"hash":"b039efe731ad111bc1b0ef221c3849d0",'
|
|
||||||
'"bytes":64,'
|
|
||||||
'"content_type":"application\/octet-stream",'
|
|
||||||
'"last_modified":"2007-03-04 20:32:17"}')
|
|
||||||
objects.append('{"name":"object7",'
|
|
||||||
'"hash":"4281c348eaf83e70ddce0e07221c3d28",'
|
|
||||||
'"bytes":14,'
|
|
||||||
'"content_type":"application\/octet-stream",'
|
|
||||||
'"last_modified":"2007-03-04 20:32:17"}')
|
|
||||||
objects.append('{"name":"object8",'
|
|
||||||
'"hash":"b039efe731ad111bc1b0ef221c3849d0",'
|
|
||||||
'"bytes":64,'
|
|
||||||
'"content_type":"application\/octet-stream",'
|
|
||||||
'"last_modified":"2007-03-04 20:32:17"}')
|
|
||||||
output = '[\n%s\n]\n' % (',\n'.join(objects[left:right]))
|
|
||||||
elif 'format' in args and args['format'] == 'xml':
|
|
||||||
objects = []
|
|
||||||
objects.append('<object><name>object1</name>'
|
|
||||||
'<hash>4281c348eaf83e70ddce0e07221c3d28</hash>'
|
|
||||||
'<bytes>14</bytes>'
|
|
||||||
'<content_type>application/octet-stream</content_type>'
|
|
||||||
'<last_modified>2007-03-04 20:32:17</last_modified>'
|
|
||||||
'</object>\n')
|
|
||||||
objects.append('<object><name>object2</name>'
|
|
||||||
'<hash>b039efe731ad111bc1b0ef221c3849d0</hash>'
|
|
||||||
'<bytes>64</bytes>'
|
|
||||||
'<content_type>application/octet-stream</content_type>'
|
|
||||||
'<last_modified>2007-03-04 20:32:17</last_modified>'
|
|
||||||
'</object>\n')
|
|
||||||
objects.append('<object><name>object3</name>'
|
|
||||||
'<hash>4281c348eaf83e70ddce0e07221c3d28</hash>'
|
|
||||||
'<bytes>14</bytes>'
|
|
||||||
'<content_type>application/octet-stream</content_type>'
|
|
||||||
'<last_modified>2007-03-04 20:32:17</last_modified>'
|
|
||||||
'</object>\n')
|
|
||||||
objects.append('<object><name>object4</name>'
|
|
||||||
'<hash>b039efe731ad111bc1b0ef221c3849d0</hash>'
|
|
||||||
'<bytes>64</bytes>'
|
|
||||||
'<content_type>application/octet-stream</content_type>'
|
|
||||||
'<last_modified>2007-03-04 20:32:17</last_modified>'
|
|
||||||
'</object>\n')
|
|
||||||
objects.append('<object><name>object5</name>'
|
|
||||||
'<hash>4281c348eaf83e70ddce0e07221c3d28</hash>'
|
|
||||||
'<bytes>14</bytes>'
|
|
||||||
'<content_type>application/octet-stream</content_type>'
|
|
||||||
'<last_modified>2007-03-04 20:32:17</last_modified>'
|
|
||||||
'</object>\n')
|
|
||||||
objects.append('<object><name>object6</name>'
|
|
||||||
'<hash>b039efe731ad111bc1b0ef221c3849d0</hash>'
|
|
||||||
'<bytes>64</bytes>'
|
|
||||||
'<content_type>application/octet-stream</content_type>'
|
|
||||||
'<last_modified>2007-03-04 20:32:17</last_modified>'
|
|
||||||
'</object>\n')
|
|
||||||
objects.append('<object><name>object7</name>'
|
|
||||||
'<hash>4281c348eaf83e70ddce0e07221c3d28</hash>'
|
|
||||||
'<bytes>14</bytes>'
|
|
||||||
'<content_type>application/octet-stream</content_type>'
|
|
||||||
'<last_modified>2007-03-04 20:32:17</last_modified>'
|
|
||||||
'</object>\n')
|
|
||||||
objects.append('<object><name>object8</name>'
|
|
||||||
'<hash>b039efe731ad111bc1b0ef221c3849d0</hash>'
|
|
||||||
'<bytes>64</bytes>'
|
|
||||||
'<content_type>application/octet-stream</content_type>'
|
|
||||||
'<last_modified>2007-03-04 20:32:17</last_modified>'
|
|
||||||
'</object>\n')
|
|
||||||
objects = objects[left:right]
|
|
||||||
objects.insert(0, '<?xml version="1.0" encoding="UTF-8"?>\n')
|
|
||||||
objects.insert(1, '<container name="test_container_1"\n')
|
|
||||||
objects.append('</container>\n')
|
|
||||||
output = ''.join(objects)
|
|
||||||
else:
|
|
||||||
objects = ['object%s\n' % i for i in range(1, 9)]
|
|
||||||
objects = objects[left:right]
|
|
||||||
output = ''.join(objects)
|
|
||||||
|
|
||||||
# prefix/path don't make much sense given our test data
|
|
||||||
if 'prefix' in args or 'path' in args:
|
|
||||||
pass
|
|
||||||
return output
|
|
||||||
|
|
||||||
def render_GET(self, path, args):
|
|
||||||
# Special path that returns 404 Not Found
|
|
||||||
if (len(path) == 4) and (path[3] == 'bogus'):
|
|
||||||
self.write('HTTP/1.1 404 Not Found\n')
|
|
||||||
self.write('Content-Type: text/plain\n')
|
|
||||||
self.write('Content-Length: 0\n')
|
|
||||||
self.write('Connection: close\n\n')
|
|
||||||
return
|
|
||||||
|
|
||||||
self.write('HTTP/1.1 200 Ok\n')
|
|
||||||
self.write('Content-Type: text/plain\n')
|
|
||||||
if len(path) == 2:
|
|
||||||
content = self._create_GET_account_content(path, args)
|
|
||||||
elif len(path) == 3:
|
|
||||||
content = self._create_GET_container_content(path, args)
|
|
||||||
# Object
|
|
||||||
elif len(path) == 4:
|
|
||||||
content = 'I am a teapot, short and stout\n'
|
|
||||||
self.write('Content-Length: %d\n' % len(content))
|
|
||||||
self.write('Connection: close\n\n')
|
|
||||||
self.write(content)
|
|
||||||
|
|
||||||
def render_HEAD(self, path, args):
|
|
||||||
# Account
|
|
||||||
if len(path) == 2:
|
|
||||||
self.write('HTTP/1.1 204 No Content\n')
|
|
||||||
self.write('Content-Type: text/plain\n')
|
|
||||||
self.write('Connection: close\n')
|
|
||||||
self.write('X-Account-Container-Count: 3\n')
|
|
||||||
self.write('X-Account-Bytes-Used: 234\n\n')
|
|
||||||
else:
|
|
||||||
self.write('HTTP/1.1 200 Ok\n')
|
|
||||||
self.write('Content-Type: text/plain\n')
|
|
||||||
self.write('ETag: d5c7f3babf6c602a8da902fb301a9f27\n')
|
|
||||||
self.write('Content-Length: 21\n')
|
|
||||||
self.write('Connection: close\n\n')
|
|
||||||
|
|
||||||
def render_POST(self, path, args):
|
|
||||||
self.write('HTTP/1.1 202 Ok\n')
|
|
||||||
self.write('Connection: close\n\n')
|
|
||||||
|
|
||||||
def render_PUT(self, path, args):
|
|
||||||
self.write('HTTP/1.1 200 Ok\n')
|
|
||||||
self.write('Content-Type: text/plain\n')
|
|
||||||
self.write('Connection: close\n\n')
|
|
||||||
render_DELETE = render_PUT
|
|
||||||
|
|
||||||
def render(self, method, uri):
|
|
||||||
if '?' in uri:
|
|
||||||
parts = uri.split('?')
|
|
||||||
query = parts[1].strip('&').split('&')
|
|
||||||
args = dict([tuple(i.split('=', 1)) for i in query])
|
|
||||||
path = parts[0].strip('/').split('/')
|
|
||||||
else:
|
|
||||||
args = {}
|
|
||||||
path = uri.strip('/').split('/')
|
|
||||||
|
|
||||||
if hasattr(self, 'render_%s' % method):
|
|
||||||
getattr(self, 'render_%s' % method)(path, args)
|
|
||||||
else:
|
|
||||||
self.write('HTTP/1.1 406 Not Acceptable\n')
|
|
||||||
self.write('Content-Type: text/plain\n')
|
|
||||||
self.write('Connection: close\n')
|
|
||||||
|
|
||||||
def makefile(self, mode, flags):
|
|
||||||
self._rbuffer.seek(0)
|
|
||||||
lines = self.read().splitlines()
|
|
||||||
(method, uri, version) = lines[0].split()
|
|
||||||
|
|
||||||
self.render(method, uri)
|
|
||||||
|
|
||||||
self._wbuffer.seek(0)
|
|
||||||
return self._wbuffer
|
|
||||||
|
|
||||||
|
|
||||||
class CustomHTTPConnection(connbase):
|
|
||||||
def connect(self):
|
|
||||||
self.sock = TrackerSocket()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
conn = CustomHTTPConnection('localhost', 8000)
|
|
||||||
conn.request('HEAD', '/v1/account/container/object')
|
|
||||||
response = conn.getresponse()
|
|
||||||
print "Status:", response.status, response.reason
|
|
||||||
for (key, value) in response.getheaders():
|
|
||||||
print "%s: %s" % (key, value)
|
|
||||||
print response.read()
|
|
135
tests/unit/test_filesystem_store.py
Normal file
135
tests/unit/test_filesystem_store.py
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||||
|
|
||||||
|
# Copyright 2011 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.
|
||||||
|
|
||||||
|
"""Tests the filesystem backend store"""
|
||||||
|
|
||||||
|
import StringIO
|
||||||
|
import unittest
|
||||||
|
import urlparse
|
||||||
|
|
||||||
|
import stubout
|
||||||
|
|
||||||
|
from glance.common import exception
|
||||||
|
from glance.store.filesystem import FilesystemBackend, ChunkedFile
|
||||||
|
from tests import stubs
|
||||||
|
|
||||||
|
|
||||||
|
class TestFilesystemBackend(unittest.TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
"""Establish a clean test environment"""
|
||||||
|
self.stubs = stubout.StubOutForTesting()
|
||||||
|
stubs.stub_out_filesystem_backend()
|
||||||
|
self.orig_chunksize = ChunkedFile.CHUNKSIZE
|
||||||
|
ChunkedFile.CHUNKSIZE = 10
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
"""Clear the test environment"""
|
||||||
|
stubs.clean_out_fake_filesystem_backend()
|
||||||
|
self.stubs.UnsetAll()
|
||||||
|
ChunkedFile.CHUNKSIZE = self.orig_chunksize
|
||||||
|
|
||||||
|
def test_get(self):
|
||||||
|
"""Test a "normal" retrieval of an image in chunks"""
|
||||||
|
url_pieces = urlparse.urlparse("file:///tmp/glance-tests/2")
|
||||||
|
image_file = FilesystemBackend.get(url_pieces)
|
||||||
|
|
||||||
|
expected_data = "chunk00000remainder"
|
||||||
|
expected_num_chunks = 2
|
||||||
|
data = ""
|
||||||
|
num_chunks = 0
|
||||||
|
|
||||||
|
for chunk in image_file:
|
||||||
|
num_chunks += 1
|
||||||
|
data += chunk
|
||||||
|
self.assertEqual(expected_data, data)
|
||||||
|
self.assertEqual(expected_num_chunks, num_chunks)
|
||||||
|
|
||||||
|
def test_get_non_existing(self):
|
||||||
|
"""
|
||||||
|
Test that trying to retrieve a file that doesn't exist
|
||||||
|
raises an error
|
||||||
|
"""
|
||||||
|
url_pieces = urlparse.urlparse("file:///tmp/glance-tests/non-existing")
|
||||||
|
self.assertRaises(exception.NotFound,
|
||||||
|
FilesystemBackend.get,
|
||||||
|
url_pieces)
|
||||||
|
|
||||||
|
def test_add(self):
|
||||||
|
"""Test that we can add an image via the filesystem backend"""
|
||||||
|
ChunkedFile.CHUNKSIZE = 1024
|
||||||
|
expected_image_id = 42
|
||||||
|
expected_file_size = 1024 * 5 # 5K
|
||||||
|
expected_file_contents = "*" * expected_file_size
|
||||||
|
expected_location = "file://%s/%s" % (stubs.FAKE_FILESYSTEM_ROOTDIR,
|
||||||
|
expected_image_id)
|
||||||
|
image_file = StringIO.StringIO(expected_file_contents)
|
||||||
|
options = {'verbose': True,
|
||||||
|
'debug': True,
|
||||||
|
'filesystem_store_datadir': stubs.FAKE_FILESYSTEM_ROOTDIR}
|
||||||
|
|
||||||
|
location, size = FilesystemBackend.add(42, image_file, options)
|
||||||
|
|
||||||
|
self.assertEquals(expected_location, location)
|
||||||
|
self.assertEquals(expected_file_size, size)
|
||||||
|
|
||||||
|
url_pieces = urlparse.urlparse("file:///tmp/glance-tests/42")
|
||||||
|
new_image_file = FilesystemBackend.get(url_pieces)
|
||||||
|
new_image_contents = ""
|
||||||
|
new_image_file_size = 0
|
||||||
|
|
||||||
|
for chunk in new_image_file:
|
||||||
|
new_image_file_size += len(chunk)
|
||||||
|
new_image_contents += chunk
|
||||||
|
|
||||||
|
self.assertEquals(expected_file_contents, new_image_contents)
|
||||||
|
self.assertEquals(expected_file_size, new_image_file_size)
|
||||||
|
|
||||||
|
def test_add_already_existing(self):
|
||||||
|
"""
|
||||||
|
Tests that adding an image with an existing identifier
|
||||||
|
raises an appropriate exception
|
||||||
|
"""
|
||||||
|
image_file = StringIO.StringIO("nevergonnamakeit")
|
||||||
|
options = {'verbose': True,
|
||||||
|
'debug': True,
|
||||||
|
'filesystem_store_datadir': stubs.FAKE_FILESYSTEM_ROOTDIR}
|
||||||
|
self.assertRaises(exception.Duplicate,
|
||||||
|
FilesystemBackend.add,
|
||||||
|
2, image_file, options)
|
||||||
|
|
||||||
|
def test_delete(self):
|
||||||
|
"""
|
||||||
|
Test we can delete an existing image in the filesystem store
|
||||||
|
"""
|
||||||
|
url_pieces = urlparse.urlparse("file:///tmp/glance-tests/2")
|
||||||
|
|
||||||
|
FilesystemBackend.delete(url_pieces)
|
||||||
|
|
||||||
|
self.assertRaises(exception.NotFound,
|
||||||
|
FilesystemBackend.get,
|
||||||
|
url_pieces)
|
||||||
|
|
||||||
|
def test_delete_non_existing(self):
|
||||||
|
"""
|
||||||
|
Test that trying to delete a file that doesn't exist
|
||||||
|
raises an error
|
||||||
|
"""
|
||||||
|
url_pieces = urlparse.urlparse("file:///tmp/glance-tests/non-existing")
|
||||||
|
self.assertRaises(exception.NotFound,
|
||||||
|
FilesystemBackend.delete,
|
||||||
|
url_pieces)
|
@ -40,27 +40,6 @@ class TestBackend(unittest.TestCase):
|
|||||||
self.stubs.UnsetAll()
|
self.stubs.UnsetAll()
|
||||||
|
|
||||||
|
|
||||||
class TestFilesystemBackend(TestBackend):
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
"""Establish a clean test environment"""
|
|
||||||
stubs.stub_out_filesystem_backend()
|
|
||||||
|
|
||||||
def tearDown(self):
|
|
||||||
"""Clear the test environment"""
|
|
||||||
stubs.clean_out_fake_filesystem_backend()
|
|
||||||
|
|
||||||
def test_get(self):
|
|
||||||
|
|
||||||
fetcher = get_from_backend("file:///tmp/glance-tests/2",
|
|
||||||
expected_size=19)
|
|
||||||
|
|
||||||
data = ""
|
|
||||||
for chunk in fetcher:
|
|
||||||
data += chunk
|
|
||||||
self.assertEqual(data, "chunk00000remainder")
|
|
||||||
|
|
||||||
|
|
||||||
class TestHTTPBackend(TestBackend):
|
class TestHTTPBackend(TestBackend):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
@ -104,45 +83,3 @@ class TestS3Backend(TestBackend):
|
|||||||
|
|
||||||
chunks = [c for c in fetcher]
|
chunks = [c for c in fetcher]
|
||||||
self.assertEqual(chunks, expected_returns)
|
self.assertEqual(chunks, expected_returns)
|
||||||
|
|
||||||
|
|
||||||
class TestSwiftBackend(TestBackend):
|
|
||||||
def setUp(self):
|
|
||||||
super(TestSwiftBackend, self).setUp()
|
|
||||||
stubs.stub_out_swift_backend(self.stubs)
|
|
||||||
|
|
||||||
def test_get(self):
|
|
||||||
|
|
||||||
swift_uri = "swift://user:pass@localhost/container1/file.tar.gz"
|
|
||||||
expected_returns = ['I ', 'am', ' a', ' t', 'ea', 'po', 't,', ' s',
|
|
||||||
'ho', 'rt', ' a', 'nd', ' s', 'to', 'ut', '\n']
|
|
||||||
|
|
||||||
fetcher = get_from_backend(swift_uri,
|
|
||||||
expected_size=21,
|
|
||||||
conn_class=SwiftBackend)
|
|
||||||
|
|
||||||
chunks = [c for c in fetcher]
|
|
||||||
|
|
||||||
self.assertEqual(chunks, expected_returns)
|
|
||||||
|
|
||||||
def test_get_bad_uri(self):
|
|
||||||
|
|
||||||
swift_url = "swift://localhost/container1/file.tar.gz"
|
|
||||||
|
|
||||||
self.assertRaises(BackendException, get_from_backend,
|
|
||||||
swift_url, expected_size=21)
|
|
||||||
|
|
||||||
def test_url_parsing(self):
|
|
||||||
|
|
||||||
swift_uri = "swift://user:pass@localhost/v1.0/container1/file.tar.gz"
|
|
||||||
|
|
||||||
parsed_uri = urlparse.urlparse(swift_uri)
|
|
||||||
|
|
||||||
(user, key, authurl, container, obj) = \
|
|
||||||
SwiftBackend._parse_swift_tokens(parsed_uri)
|
|
||||||
|
|
||||||
self.assertEqual(user, 'user')
|
|
||||||
self.assertEqual(key, 'pass')
|
|
||||||
self.assertEqual(authurl, 'https://localhost/v1.0')
|
|
||||||
self.assertEqual(container, 'container1')
|
|
||||||
self.assertEqual(obj, 'file.tar.gz')
|
|
||||||
|
261
tests/unit/test_swift_store.py
Normal file
261
tests/unit/test_swift_store.py
Normal file
@ -0,0 +1,261 @@
|
|||||||
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||||
|
|
||||||
|
# Copyright 2011 OpenStack, LLC
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this swift 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.
|
||||||
|
|
||||||
|
"""Tests the Swift backend store"""
|
||||||
|
|
||||||
|
import StringIO
|
||||||
|
import hashlib
|
||||||
|
import httplib
|
||||||
|
import sys
|
||||||
|
import unittest
|
||||||
|
import urlparse
|
||||||
|
|
||||||
|
import stubout
|
||||||
|
|
||||||
|
from glance.common import exception
|
||||||
|
import glance.store.swift
|
||||||
|
|
||||||
|
SwiftBackend = glance.store.swift.SwiftBackend
|
||||||
|
|
||||||
|
SWIFT_INSTALLED = False
|
||||||
|
|
||||||
|
try:
|
||||||
|
import swift.common.client
|
||||||
|
SWIFT_INSTALLED = True
|
||||||
|
except ImportError:
|
||||||
|
print "Skipping Swift store tests since Swift is not installed."
|
||||||
|
|
||||||
|
FIVE_KB = (5 * 1024)
|
||||||
|
SWIFT_OPTIONS = {'verbose': True,
|
||||||
|
'debug': True,
|
||||||
|
'swift_store_user': 'glance',
|
||||||
|
'swift_store_key': 'key',
|
||||||
|
'swift_store_auth_address': 'localhost:8080',
|
||||||
|
'swift_store_container': 'glance'}
|
||||||
|
|
||||||
|
|
||||||
|
# We stub out as little as possible to ensure that the code paths
|
||||||
|
# between glance.store.swift and swift.common.client are tested
|
||||||
|
# thoroughly
|
||||||
|
def stub_out_swift_common_client(stubs):
|
||||||
|
|
||||||
|
fixture_headers = {'glance/2':
|
||||||
|
{'content-length': FIVE_KB,
|
||||||
|
'etag': 'c2e5db72bd7fd153f53ede5da5a06de3'}}
|
||||||
|
fixture_objects = {'glance/2':
|
||||||
|
StringIO.StringIO("*" * FIVE_KB)}
|
||||||
|
|
||||||
|
def fake_put_object(url, token, container, name, contents, **kwargs):
|
||||||
|
# PUT returns the ETag header for the newly-added object
|
||||||
|
fixture_key = "%s/%s" % (container, name)
|
||||||
|
if not fixture_key in fixture_headers.keys():
|
||||||
|
if hasattr(contents, 'read'):
|
||||||
|
fixture_object = StringIO.StringIO()
|
||||||
|
chunk = contents.read(SwiftBackend.CHUNKSIZE)
|
||||||
|
while chunk:
|
||||||
|
fixture_object.write(chunk)
|
||||||
|
chunk = contents.read(SwiftBackend.CHUNKSIZE)
|
||||||
|
else:
|
||||||
|
fixture_object = StringIO.StringIO(contents)
|
||||||
|
fixture_objects[fixture_key] = fixture_object
|
||||||
|
fixture_headers[fixture_key] = {
|
||||||
|
'content-length': fixture_object.len,
|
||||||
|
'etag': hashlib.md5(fixture_object.read()).hexdigest()}
|
||||||
|
return fixture_headers[fixture_key]['etag']
|
||||||
|
else:
|
||||||
|
msg = ("Object PUT failed - Object with key %s already exists"
|
||||||
|
% fixture_key)
|
||||||
|
raise swift.common.client.ClientException(msg,
|
||||||
|
http_status=httplib.CONFLICT)
|
||||||
|
|
||||||
|
def fake_get_object(url, token, container, name, **kwargs):
|
||||||
|
# GET returns the tuple (list of headers, file object)
|
||||||
|
try:
|
||||||
|
fixture_key = "%s/%s" % (container, name)
|
||||||
|
return fixture_headers[fixture_key], fixture_objects[fixture_key]
|
||||||
|
except KeyError:
|
||||||
|
msg = "Object GET failed"
|
||||||
|
raise swift.common.client.ClientException(msg,
|
||||||
|
http_status=httplib.NOT_FOUND)
|
||||||
|
|
||||||
|
def fake_head_object(url, token, container, name, **kwargs):
|
||||||
|
# HEAD returns the list of headers for an object
|
||||||
|
try:
|
||||||
|
fixture_key = "%s/%s" % (container, name)
|
||||||
|
return fixture_headers[fixture_key]
|
||||||
|
except KeyError:
|
||||||
|
msg = "Object HEAD failed - Object does not exist"
|
||||||
|
raise swift.common.client.ClientException(msg,
|
||||||
|
http_status=httplib.NOT_FOUND)
|
||||||
|
|
||||||
|
def fake_delete_object(url, token, container, name, **kwargs):
|
||||||
|
# DELETE returns nothing
|
||||||
|
fixture_key = "%s/%s" % (container, name)
|
||||||
|
if fixture_key not in fixture_headers.keys():
|
||||||
|
msg = "Object DELETE failed - Object does not exist"
|
||||||
|
raise swift.common.client.ClientException(msg,
|
||||||
|
http_status=httplib.NOT_FOUND)
|
||||||
|
else:
|
||||||
|
del fixture_headers[fixture_key]
|
||||||
|
del fixture_objects[fixture_key]
|
||||||
|
|
||||||
|
def fake_get_connection_class(*args):
|
||||||
|
return swift.common.client.Connection
|
||||||
|
|
||||||
|
def fake_http_connection(self):
|
||||||
|
return None
|
||||||
|
|
||||||
|
def fake_get_auth(self):
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
stubs.Set(swift.common.client,
|
||||||
|
'put_object', fake_put_object)
|
||||||
|
stubs.Set(swift.common.client,
|
||||||
|
'delete_object', fake_delete_object)
|
||||||
|
stubs.Set(swift.common.client,
|
||||||
|
'head_object', fake_head_object)
|
||||||
|
stubs.Set(swift.common.client,
|
||||||
|
'get_object', fake_get_object)
|
||||||
|
stubs.Set(swift.common.client.Connection,
|
||||||
|
'get_auth', fake_get_auth)
|
||||||
|
stubs.Set(swift.common.client.Connection,
|
||||||
|
'http_connection', fake_http_connection)
|
||||||
|
stubs.Set(glance.store.swift,
|
||||||
|
'get_connection_class', fake_get_connection_class)
|
||||||
|
|
||||||
|
|
||||||
|
class TestSwiftBackend(unittest.TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
"""Establish a clean test environment"""
|
||||||
|
self.stubs = stubout.StubOutForTesting()
|
||||||
|
if SWIFT_INSTALLED:
|
||||||
|
stub_out_swift_common_client(self.stubs)
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
"""Clear the test environment"""
|
||||||
|
self.stubs.UnsetAll()
|
||||||
|
|
||||||
|
def test_get(self):
|
||||||
|
"""Test a "normal" retrieval of an image in chunks"""
|
||||||
|
if not SWIFT_INSTALLED:
|
||||||
|
return
|
||||||
|
url_pieces = urlparse.urlparse(
|
||||||
|
"swift://user:key@auth_address/glance/2")
|
||||||
|
image_swift = SwiftBackend.get(url_pieces)
|
||||||
|
|
||||||
|
expected_data = "*" * FIVE_KB
|
||||||
|
data = ""
|
||||||
|
|
||||||
|
for chunk in image_swift:
|
||||||
|
data += chunk
|
||||||
|
self.assertEqual(expected_data, data)
|
||||||
|
|
||||||
|
def test_get_mismatched_expected_size(self):
|
||||||
|
"""
|
||||||
|
Test retrieval of an image with wrong expected_size param
|
||||||
|
raises an exception
|
||||||
|
"""
|
||||||
|
if not SWIFT_INSTALLED:
|
||||||
|
return
|
||||||
|
url_pieces = urlparse.urlparse(
|
||||||
|
"swift://user:key@auth_address/glance/2")
|
||||||
|
self.assertRaises(glance.store.BackendException,
|
||||||
|
SwiftBackend.get,
|
||||||
|
url_pieces,
|
||||||
|
{'expected_size': 42})
|
||||||
|
|
||||||
|
def test_get_non_existing(self):
|
||||||
|
"""
|
||||||
|
Test that trying to retrieve a swift that doesn't exist
|
||||||
|
raises an error
|
||||||
|
"""
|
||||||
|
if not SWIFT_INSTALLED:
|
||||||
|
return
|
||||||
|
url_pieces = urlparse.urlparse(
|
||||||
|
"swift://user:key@auth_address/noexist")
|
||||||
|
self.assertRaises(exception.NotFound,
|
||||||
|
SwiftBackend.get,
|
||||||
|
url_pieces)
|
||||||
|
|
||||||
|
def test_add(self):
|
||||||
|
"""Test that we can add an image via the swift backend"""
|
||||||
|
if not SWIFT_INSTALLED:
|
||||||
|
return
|
||||||
|
expected_image_id = 42
|
||||||
|
expected_swift_size = 1024 * 5 # 5K
|
||||||
|
expected_swift_contents = "*" * expected_swift_size
|
||||||
|
expected_location = "swift://%s:%s@%s/%s/%s" % (
|
||||||
|
SWIFT_OPTIONS['swift_store_user'],
|
||||||
|
SWIFT_OPTIONS['swift_store_key'],
|
||||||
|
SWIFT_OPTIONS['swift_store_auth_address'],
|
||||||
|
SWIFT_OPTIONS['swift_store_container'],
|
||||||
|
expected_image_id)
|
||||||
|
image_swift = StringIO.StringIO(expected_swift_contents)
|
||||||
|
|
||||||
|
location, size = SwiftBackend.add(42, image_swift, SWIFT_OPTIONS)
|
||||||
|
|
||||||
|
self.assertEquals(expected_location, location)
|
||||||
|
self.assertEquals(expected_swift_size, size)
|
||||||
|
|
||||||
|
url_pieces = urlparse.urlparse(
|
||||||
|
"swift://user:key@auth_address/glance/42")
|
||||||
|
new_image_swift = SwiftBackend.get(url_pieces)
|
||||||
|
new_image_contents = new_image_swift.getvalue()
|
||||||
|
new_image_swift_size = new_image_swift.len
|
||||||
|
|
||||||
|
self.assertEquals(expected_swift_contents, new_image_contents)
|
||||||
|
self.assertEquals(expected_swift_size, new_image_swift_size)
|
||||||
|
|
||||||
|
def test_add_already_existing(self):
|
||||||
|
"""
|
||||||
|
Tests that adding an image with an existing identifier
|
||||||
|
raises an appropriate exception
|
||||||
|
"""
|
||||||
|
if not SWIFT_INSTALLED:
|
||||||
|
return
|
||||||
|
image_swift = StringIO.StringIO("nevergonnamakeit")
|
||||||
|
self.assertRaises(exception.Duplicate,
|
||||||
|
SwiftBackend.add,
|
||||||
|
2, image_swift, SWIFT_OPTIONS)
|
||||||
|
|
||||||
|
def test_delete(self):
|
||||||
|
"""
|
||||||
|
Test we can delete an existing image in the swift store
|
||||||
|
"""
|
||||||
|
if not SWIFT_INSTALLED:
|
||||||
|
return
|
||||||
|
url_pieces = urlparse.urlparse(
|
||||||
|
"swift://user:key@auth_address/glance/2")
|
||||||
|
|
||||||
|
SwiftBackend.delete(url_pieces)
|
||||||
|
|
||||||
|
self.assertRaises(exception.NotFound,
|
||||||
|
SwiftBackend.get,
|
||||||
|
url_pieces)
|
||||||
|
|
||||||
|
def test_delete_non_existing(self):
|
||||||
|
"""
|
||||||
|
Test that trying to delete a swift that doesn't exist
|
||||||
|
raises an error
|
||||||
|
"""
|
||||||
|
if not SWIFT_INSTALLED:
|
||||||
|
return
|
||||||
|
url_pieces = urlparse.urlparse("swift://user:key@auth_address/noexist")
|
||||||
|
self.assertRaises(exception.NotFound,
|
||||||
|
SwiftBackend.delete,
|
||||||
|
url_pieces)
|
@ -12,5 +12,6 @@ nose
|
|||||||
sphinx
|
sphinx
|
||||||
argparse
|
argparse
|
||||||
mox==0.5.0
|
mox==0.5.0
|
||||||
|
swift
|
||||||
-f http://pymox.googlecode.com/files/mox-0.5.0.tar.gz
|
-f http://pymox.googlecode.com/files/mox-0.5.0.tar.gz
|
||||||
sqlalchemy-migrate>=0.6
|
sqlalchemy-migrate>=0.6
|
||||||
|
Loading…
Reference in New Issue
Block a user