Fixed review stuff from Brian
This commit is contained in:
commit
684d89117e
2
Authors
2
Authors
@ -7,11 +7,13 @@ Donal Lafferty <donal.lafferty@citrix.com>
|
||||
Eldar Nugaev <enugaev@griddynamics.com>
|
||||
Ewan Mellor <ewan.mellor@citrix.com>
|
||||
Isaku Yamahata <yamahata@valinux.co.jp>
|
||||
Jason Koelker <jason@koelker.net>
|
||||
Jay Pipes <jaypipes@gmail.com>
|
||||
Jinwoo 'Joseph' Suh <jsuh@isi.edu>
|
||||
Josh Kearney <josh@jk0.org>
|
||||
Justin Shepherd <jshepher@rackspace.com>
|
||||
Ken Pepple <ken.pepple@gmail.com>
|
||||
Kevin L. Mitchell <kevin.mitchell@rackspace.com>
|
||||
Matt Dietz <matt.dietz@rackspace.com>
|
||||
Monty Taylor <mordred@inaugust.com>
|
||||
Rick Clark <rick@openstack.org>
|
||||
|
@ -44,15 +44,16 @@ from glance.common import config
|
||||
|
||||
ALL_COMMANDS = ['start', 'stop', 'shutdown', 'restart',
|
||||
'reload', 'force-reload']
|
||||
ALL_SERVERS = ['glance-api', 'glance-registry']
|
||||
GRACEFUL_SHUTDOWN_SERVERS = ['glance-api', 'glance-registry']
|
||||
ALL_SERVERS = ['glance-api', 'glance-registry', 'glance-scrubber']
|
||||
GRACEFUL_SHUTDOWN_SERVERS = ['glance-api', 'glance-registry',
|
||||
'glance-scrubber']
|
||||
MAX_DESCRIPTORS = 32768
|
||||
MAX_MEMORY = (1024 * 1024 * 1024) * 2 # 2 GB
|
||||
USAGE = """%prog [options] <SERVER> <COMMAND> [CONFPATH]
|
||||
|
||||
Where <SERVER> is one of:
|
||||
|
||||
all, api, registry
|
||||
all, api, registry, scrubber
|
||||
|
||||
And command is one of:
|
||||
|
||||
|
80
bin/glance-scrubber
Executable file
80
bin/glance-scrubber
Executable file
@ -0,0 +1,80 @@
|
||||
#!/usr/bin/env python
|
||||
# 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.
|
||||
|
||||
"""
|
||||
Glance Scrub Service
|
||||
"""
|
||||
|
||||
import optparse
|
||||
import os
|
||||
import sys
|
||||
|
||||
# If ../glance/__init__.py exists, add ../ to Python search path, so that
|
||||
# it will override what happens to be installed in /usr/(local/)lib/python...
|
||||
possible_topdir = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]),
|
||||
os.pardir,
|
||||
os.pardir))
|
||||
if os.path.exists(os.path.join(possible_topdir, 'glance', '__init__.py')):
|
||||
sys.path.insert(0, possible_topdir)
|
||||
|
||||
from glance import version
|
||||
from glance.common import config
|
||||
from glance.store import scrubber
|
||||
|
||||
|
||||
def create_options(parser):
|
||||
"""
|
||||
Sets up the CLI and config-file options that may be
|
||||
parsed and program commands.
|
||||
|
||||
:param parser: The option parser
|
||||
"""
|
||||
config.add_common_options(parser)
|
||||
config.add_log_options(parser)
|
||||
parser.add_option("-D", "--daemon", default=False, dest="daemon",
|
||||
action="store_true",
|
||||
help="Run as a long-running process. When not "
|
||||
"specified (the default) run the scrub "
|
||||
"operation once and then exits. When specified "
|
||||
"do not exit and run scrub on wakeup_time "
|
||||
"interval as specified in the config file.")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
oparser = optparse.OptionParser(version='%%prog %s'
|
||||
% version.version_string())
|
||||
create_options(oparser)
|
||||
(options, args) = config.parse_options(oparser)
|
||||
|
||||
try:
|
||||
conf, app = config.load_paste_app('glance-scrubber', options, args)
|
||||
daemon = options.get('daemon') or \
|
||||
config.get_option(conf, 'daemon', type='bool',
|
||||
default=False)
|
||||
|
||||
if daemon:
|
||||
wakeup_time = int(conf.get('wakeup_time', 300))
|
||||
server = scrubber.Daemon(wakeup_time)
|
||||
server.start(app)
|
||||
server.wait()
|
||||
else:
|
||||
import eventlet
|
||||
pool = eventlet.greenpool.GreenPool(1000)
|
||||
scrubber = app.run(pool)
|
||||
except RuntimeError, e:
|
||||
sys.exit("ERROR: %s" % e)
|
@ -150,6 +150,9 @@ Can only be specified in configuration files.
|
||||
Sets the storage backend to use by default when storing images in Glance.
|
||||
Available options for this option are (``file``, ``swift``, or ``s3``).
|
||||
|
||||
Configuring the Filesystem Storage Backend
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
* ``filesystem_store_datadir=PATH``
|
||||
|
||||
Optional. Default: ``/var/lib/glance/images/``
|
||||
@ -163,6 +166,9 @@ the filesystem storage backend will attempt to create this directory if it does
|
||||
not exist. Ensure that the user that ``glance-api`` runs under has write
|
||||
permissions to this directory.
|
||||
|
||||
Configuring the Swift Storage Backend
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
* ``swift_store_auth_address=URL``
|
||||
|
||||
Required when using the Swift storage backend.
|
||||
@ -219,6 +225,82 @@ Can only be specified in configuration files.
|
||||
If true, Glance will attempt to create the container ``swift_store_container``
|
||||
if it does not exist.
|
||||
|
||||
Configuring the S3 Storage Backend
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
* ``s3_store_host=URL``
|
||||
|
||||
Required when using the S3 storage backend.
|
||||
|
||||
Can only be specified in configuration files.
|
||||
|
||||
`This option is specific to the S3 storage backend.`
|
||||
|
||||
Default: s3.amazonaws.com
|
||||
|
||||
Sets the main service URL supplied to S3 when making calls to its storage
|
||||
system. For more information about the S3 authentication system, please
|
||||
see the `S3 documentation <http://aws.amazon.com/documentation/s3/>`_
|
||||
|
||||
* ``s3_store_access_key=ACCESS_KEY``
|
||||
|
||||
Required when using the S3 storage backend.
|
||||
|
||||
Can only be specified in configuration files.
|
||||
|
||||
`This option is specific to the S3 storage backend.`
|
||||
|
||||
Sets the access key to authenticate against the ``s3_store_host`` with.
|
||||
|
||||
You should set this to your 20-character Amazon AWS access key.
|
||||
|
||||
* ``s3_store_secret_key=SECRET_KEY``
|
||||
|
||||
Required when using the S3 storage backend.
|
||||
|
||||
Can only be specified in configuration files.
|
||||
|
||||
`This option is specific to the S3 storage backend.`
|
||||
|
||||
Sets the secret key to authenticate against the
|
||||
``s3_store_host`` with for the access key ``s3_store_access_key``.
|
||||
|
||||
You should set this to your 40-character Amazon AWS secret key.
|
||||
|
||||
* ``s3_store_bucket=BUCKET``
|
||||
|
||||
Required when using the S3 storage backend.
|
||||
|
||||
Can only be specified in configuration files.
|
||||
|
||||
`This option is specific to the S3 storage backend.`
|
||||
|
||||
Sets the name of the bucket to use for Glance images in S3.
|
||||
|
||||
Note that the namespace for S3 buckets is **global**, and
|
||||
therefore you must use a name for the bucket that is unique. It
|
||||
is recommended that you use a combination of your AWS access key,
|
||||
**lowercased** with "+glance".
|
||||
|
||||
For instance if your Amazon AWS access key is:
|
||||
|
||||
``ABCDEFGHIJKLMNOPQRST``
|
||||
|
||||
then make your bucket value be:
|
||||
|
||||
``abcdefghijklmnopqrst+glance``
|
||||
|
||||
* ``s3_store_create_bucket_on_put``
|
||||
|
||||
Optional. Default: ``False``
|
||||
|
||||
Can only be specified in configuration files.
|
||||
|
||||
`This option is specific to the S3 storage backend.`
|
||||
|
||||
If true, Glance will attempt to create the bucket ``s3_store_bucket``
|
||||
if it does not exist.
|
||||
|
||||
Configuring the Glance Registry
|
||||
-------------------------------
|
||||
|
||||
|
@ -66,14 +66,21 @@ s3_store_secret_key = <40-char AWS secret key>
|
||||
# Container within the account that the account should use
|
||||
# for storing images in S3. Note that S3 has a flat namespace,
|
||||
# so you need a unique bucket name for your glance images. An
|
||||
# easy way to do this is append your AWS access key to "+glance"
|
||||
s3_store_bucket = <20-char AWS access key>+glance
|
||||
# easy way to do this is append your AWS access key to "+glance".
|
||||
# S3 buckets in AWS *must* be lowercased, so remember to lowercase
|
||||
# your AWS access key if you use it in your bucket name below!
|
||||
s3_store_bucket = <lowercased 20-char aws access key>+glance
|
||||
|
||||
# Do we create the bucket if it does not exist?
|
||||
s3_store_create_bucket_on_put = False
|
||||
|
||||
# ============ Delayed Delete Options =============================
|
||||
|
||||
# Turn on/off delayed delete
|
||||
delayed_delete = False
|
||||
|
||||
[pipeline:glance-api]
|
||||
pipeline = versionnegotiation apiv1app
|
||||
pipeline = versionnegotiation context apiv1app
|
||||
|
||||
[pipeline:versions]
|
||||
pipeline = versionsapp
|
||||
@ -86,3 +93,6 @@ paste.app_factory = glance.api.v1:app_factory
|
||||
|
||||
[filter:versionnegotiation]
|
||||
paste.filter_factory = glance.api.middleware.version_negotiation:filter_factory
|
||||
|
||||
[filter:context]
|
||||
paste.filter_factory = glance.common.context:filter_factory
|
||||
|
@ -29,5 +29,11 @@ sql_connection = sqlite:///glance.sqlite
|
||||
# before MySQL can drop the connection.
|
||||
sql_idle_timeout = 3600
|
||||
|
||||
[app:glance-registry]
|
||||
[pipeline:glance-registry]
|
||||
pipeline = context registryapp
|
||||
|
||||
[app:registryapp]
|
||||
paste.app_factory = glance.registry.server:app_factory
|
||||
|
||||
[filter:context]
|
||||
paste.filter_factory = glance.common.context:filter_factory
|
||||
|
36
etc/glance-scrubber.conf
Normal file
36
etc/glance-scrubber.conf
Normal file
@ -0,0 +1,36 @@
|
||||
[DEFAULT]
|
||||
# Show more verbose log output (sets INFO log level output)
|
||||
verbose = True
|
||||
|
||||
# Show debugging output in logs (sets DEBUG log level output)
|
||||
debug = False
|
||||
|
||||
# Log to this file. Make sure you do not set the same log
|
||||
# file for both the API and registry servers!
|
||||
log_file = /var/log/glance/scrubber.log
|
||||
|
||||
# Delayed delete time in seconds
|
||||
scrub_time = 43200
|
||||
|
||||
# Should we run our own loop or rely on cron/scheduler to run us
|
||||
daemon = False
|
||||
|
||||
# Loop time between checking the db for new items to schedule for delete
|
||||
wakeup_time = 300
|
||||
|
||||
# SQLAlchemy connection string for the reference implementation
|
||||
# registry server. Any valid SQLAlchemy connection string is fine.
|
||||
# See: http://www.sqlalchemy.org/docs/05/reference/sqlalchemy/connections.html#sqlalchemy.create_engine
|
||||
sql_connection = sqlite:///glance.sqlite
|
||||
|
||||
# Period in seconds after which SQLAlchemy should reestablish its connection
|
||||
# to the database.
|
||||
#
|
||||
# MySQL uses a default `wait_timeout` of 8 hours, after which it will drop
|
||||
# idle connections. This can result in 'MySQL Gone Away' exceptions. If you
|
||||
# notice this, you can lower this value to ensure that SQLAlchemy reconnects
|
||||
# before MySQL can drop the connection.
|
||||
sql_idle_timeout = 3600
|
||||
|
||||
[app:glance-scrubber]
|
||||
paste.app_factory = glance.store.scrubber:app_factory
|
@ -27,12 +27,13 @@ import sys
|
||||
import webob
|
||||
from webob.exc import (HTTPNotFound,
|
||||
HTTPConflict,
|
||||
HTTPBadRequest)
|
||||
HTTPBadRequest,
|
||||
HTTPForbidden)
|
||||
|
||||
from glance.common import exception
|
||||
from glance.common import wsgi
|
||||
from glance.store import (get_from_backend,
|
||||
delete_from_backend,
|
||||
schedule_delete_from_backend,
|
||||
get_store_from_location,
|
||||
get_backend_class,
|
||||
UnsupportedBackend)
|
||||
@ -96,7 +97,8 @@ class Controller(object):
|
||||
"""
|
||||
params = self._get_query_params(req)
|
||||
try:
|
||||
images = registry.get_images_list(self.options, **params)
|
||||
images = registry.get_images_list(self.options, req.context,
|
||||
**params)
|
||||
except exception.Invalid, e:
|
||||
raise HTTPBadRequest(explanation=str(e))
|
||||
|
||||
@ -126,7 +128,8 @@ class Controller(object):
|
||||
"""
|
||||
params = self._get_query_params(req)
|
||||
try:
|
||||
images = registry.get_images_detail(self.options, **params)
|
||||
images = registry.get_images_detail(self.options, req.context,
|
||||
**params)
|
||||
except exception.Invalid, e:
|
||||
raise HTTPBadRequest(explanation=str(e))
|
||||
return dict(images=images)
|
||||
@ -226,6 +229,7 @@ class Controller(object):
|
||||
|
||||
try:
|
||||
image_meta = registry.add_image_metadata(self.options,
|
||||
req.context,
|
||||
image_meta)
|
||||
return image_meta
|
||||
except exception.Duplicate:
|
||||
@ -238,6 +242,11 @@ class Controller(object):
|
||||
for line in msg.split('\n'):
|
||||
logger.error(line)
|
||||
raise HTTPBadRequest(msg, request=req, content_type="text/plain")
|
||||
except exception.NotAuthorized:
|
||||
msg = "Not authorized to reserve image."
|
||||
logger.error(msg)
|
||||
raise HTTPForbidden(msg, request=req,
|
||||
content_type="text/plain")
|
||||
|
||||
def _upload(self, req, image_meta):
|
||||
"""
|
||||
@ -267,7 +276,7 @@ class Controller(object):
|
||||
|
||||
image_id = image_meta['id']
|
||||
logger.debug("Setting image %s to status 'saving'" % image_id)
|
||||
registry.update_image_metadata(self.options, image_id,
|
||||
registry.update_image_metadata(self.options, req.context, image_id,
|
||||
{'status': 'saving'})
|
||||
try:
|
||||
logger.debug("Uploading image data for image %(image_id)s "
|
||||
@ -298,7 +307,8 @@ class Controller(object):
|
||||
logger.debug("Updating image %(image_id)s data. "
|
||||
"Checksum set to %(checksum)s, size set "
|
||||
"to %(size)d" % locals())
|
||||
registry.update_image_metadata(self.options, image_id,
|
||||
registry.update_image_metadata(self.options, req.context,
|
||||
image_id,
|
||||
{'checksum': checksum,
|
||||
'size': size})
|
||||
|
||||
@ -310,6 +320,13 @@ class Controller(object):
|
||||
self._safe_kill(req, image_id)
|
||||
raise HTTPConflict(msg, request=req)
|
||||
|
||||
except exception.NotAuthorized, e:
|
||||
msg = ("Unauthorized upload attempt: %s") % str(e)
|
||||
logger.error(msg)
|
||||
self._safe_kill(req, image_id)
|
||||
raise HTTPForbidden(msg, request=req,
|
||||
content_type='text/plain')
|
||||
|
||||
except Exception, e:
|
||||
msg = ("Error uploading image: %s") % str(e)
|
||||
logger.error(msg)
|
||||
@ -329,6 +346,7 @@ class Controller(object):
|
||||
image_meta['location'] = location
|
||||
image_meta['status'] = 'active'
|
||||
return registry.update_image_metadata(self.options,
|
||||
req.context,
|
||||
image_id,
|
||||
image_meta)
|
||||
|
||||
@ -340,6 +358,7 @@ class Controller(object):
|
||||
:param image_id: Opaque image identifier
|
||||
"""
|
||||
registry.update_image_metadata(self.options,
|
||||
req.context,
|
||||
image_id,
|
||||
{'status': 'killed'})
|
||||
|
||||
@ -412,6 +431,12 @@ class Controller(object):
|
||||
and the request body is not application/octet-stream
|
||||
image data.
|
||||
"""
|
||||
if req.context.read_only:
|
||||
msg = "Read-only access"
|
||||
logger.debug(msg)
|
||||
raise HTTPForbidden(msg, request=req,
|
||||
content_type="text/plain")
|
||||
|
||||
image_meta = self._reserve(req, image_meta)
|
||||
image_id = image_meta['id']
|
||||
|
||||
@ -433,6 +458,12 @@ class Controller(object):
|
||||
|
||||
:retval Returns the updated image information as a mapping
|
||||
"""
|
||||
if req.context.read_only:
|
||||
msg = "Read-only access"
|
||||
logger.debug(msg)
|
||||
raise HTTPForbidden(msg, request=req,
|
||||
content_type="text/plain")
|
||||
|
||||
orig_image_meta = self.get_image_meta_or_404(req, id)
|
||||
orig_status = orig_image_meta['status']
|
||||
|
||||
@ -440,8 +471,9 @@ class Controller(object):
|
||||
raise HTTPConflict("Cannot upload to an unqueued image")
|
||||
|
||||
try:
|
||||
image_meta = registry.update_image_metadata(self.options, id,
|
||||
image_meta, True)
|
||||
image_meta = registry.update_image_metadata(self.options,
|
||||
req.context, id,
|
||||
image_meta, True)
|
||||
if image_data is not None:
|
||||
image_meta = self._upload_and_activate(req, image_meta)
|
||||
except exception.Invalid, e:
|
||||
@ -465,6 +497,12 @@ class Controller(object):
|
||||
:raises HttpNotAuthorized if image or any chunk is not
|
||||
deleteable by the requesting user
|
||||
"""
|
||||
if req.context.read_only:
|
||||
msg = "Read-only access"
|
||||
logger.debug(msg)
|
||||
raise HTTPForbidden(msg, request=req,
|
||||
content_type="text/plain")
|
||||
|
||||
image = self.get_image_meta_or_404(req, id)
|
||||
|
||||
# The image's location field may be None in the case
|
||||
@ -472,14 +510,9 @@ class Controller(object):
|
||||
# to delete the image if the backend doesn't yet store it.
|
||||
# See https://bugs.launchpad.net/glance/+bug/747799
|
||||
if image['location']:
|
||||
try:
|
||||
delete_from_backend(image['location'])
|
||||
except (UnsupportedBackend, exception.NotFound):
|
||||
msg = "Failed to delete image from store (%s). " + \
|
||||
"Continuing with deletion from registry."
|
||||
logger.error(msg % (image['location'],))
|
||||
|
||||
registry.delete_image_metadata(self.options, id)
|
||||
schedule_delete_from_backend(image['location'], self.options,
|
||||
req.context, id)
|
||||
registry.delete_image_metadata(self.options, req.context, id)
|
||||
|
||||
def get_image_meta_or_404(self, request, id):
|
||||
"""
|
||||
@ -492,12 +525,18 @@ class Controller(object):
|
||||
:raises HTTPNotFound if image does not exist
|
||||
"""
|
||||
try:
|
||||
return registry.get_image_metadata(self.options, id)
|
||||
return registry.get_image_metadata(self.options,
|
||||
request.context, id)
|
||||
except exception.NotFound:
|
||||
msg = "Image with identifier %s not found" % id
|
||||
logger.debug(msg)
|
||||
raise HTTPNotFound(msg, request=request,
|
||||
content_type='text/plain')
|
||||
except exception.NotAuthorized:
|
||||
msg = "Unauthorized image access"
|
||||
logger.debug(msg)
|
||||
raise HTTPForbidden(msg, request=request,
|
||||
content_type='text/plain')
|
||||
|
||||
def get_store_or_400(self, request, store_name):
|
||||
"""
|
||||
|
@ -35,7 +35,8 @@ class V1Client(base_client.BaseClient):
|
||||
|
||||
DEFAULT_PORT = 9292
|
||||
|
||||
def __init__(self, host, port=None, use_ssl=False, doc_root="/v1"):
|
||||
def __init__(self, host, port=None, use_ssl=False, doc_root="/v1",
|
||||
auth_tok=None):
|
||||
"""
|
||||
Creates a new client to a Glance API service.
|
||||
|
||||
@ -43,10 +44,11 @@ class V1Client(base_client.BaseClient):
|
||||
:param port: The port where Glance resides (defaults to 9292)
|
||||
:param use_ssl: Should we use HTTPS? (defaults to False)
|
||||
:param doc_root: Prefix for all URLs we request from host
|
||||
:param auth_tok: The auth token to pass to the server
|
||||
"""
|
||||
port = port or self.DEFAULT_PORT
|
||||
self.doc_root = doc_root
|
||||
super(Client, self).__init__(host, port, use_ssl)
|
||||
super(Client, self).__init__(host, port, use_ssl, auth_tok)
|
||||
|
||||
def do_request(self, method, action, body=None, headers=None, params=None):
|
||||
action = "%s/%s" % (self.doc_root, action.lstrip("/"))
|
||||
|
@ -41,17 +41,19 @@ class BaseClient(object):
|
||||
|
||||
CHUNKSIZE = 65536
|
||||
|
||||
def __init__(self, host, port, use_ssl):
|
||||
def __init__(self, host, port, use_ssl, auth_tok):
|
||||
"""
|
||||
Creates a new client to some service.
|
||||
|
||||
:param host: The host where service resides
|
||||
:param port: The port where service resides
|
||||
:param use_ssl: Should we use HTTPS?
|
||||
:param auth_tok: The auth token to pass to the server
|
||||
"""
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.use_ssl = use_ssl
|
||||
self.auth_tok = auth_tok
|
||||
self.connection = None
|
||||
|
||||
def get_connection_type(self):
|
||||
@ -99,6 +101,8 @@ class BaseClient(object):
|
||||
try:
|
||||
connection_type = self.get_connection_type()
|
||||
headers = headers or {}
|
||||
if 'x-auth-token' not in headers and self.auth_tok:
|
||||
headers['x-auth-token'] = self.auth_tok
|
||||
c = connection_type(self.host, self.port)
|
||||
|
||||
# Do a simple request or a chunked request, depending
|
||||
|
97
glance/common/context.py
Normal file
97
glance/common/context.py
Normal file
@ -0,0 +1,97 @@
|
||||
# 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.
|
||||
|
||||
from glance.common import utils
|
||||
from glance.common import wsgi
|
||||
|
||||
|
||||
class RequestContext(object):
|
||||
"""
|
||||
Stores information about the security context under which the user
|
||||
accesses the system, as well as additional request information.
|
||||
"""
|
||||
|
||||
def __init__(self, auth_tok=None, user=None, tenant=None, is_admin=False,
|
||||
read_only=False, show_deleted=False):
|
||||
self.auth_tok = auth_tok
|
||||
self.user = user
|
||||
self.tenant = tenant
|
||||
self.is_admin = is_admin
|
||||
self.read_only = read_only
|
||||
self.show_deleted = show_deleted
|
||||
|
||||
def is_image_visible(self, image):
|
||||
"""Return True if the image is visible in this context."""
|
||||
# Is admin == image visible
|
||||
if self.is_admin:
|
||||
return True
|
||||
|
||||
# No owner == image visible
|
||||
if image.owner is None:
|
||||
return True
|
||||
|
||||
# Image is_public == image visible
|
||||
if image.is_public:
|
||||
return True
|
||||
|
||||
# Private image
|
||||
return self.owner is not None and self.owner == image.owner
|
||||
|
||||
@property
|
||||
def owner(self):
|
||||
"""Return the owner to correlate with an image."""
|
||||
return self.tenant
|
||||
|
||||
|
||||
class ContextMiddleware(wsgi.Middleware):
|
||||
def __init__(self, app, options):
|
||||
self.options = options
|
||||
super(ContextMiddleware, self).__init__(app)
|
||||
|
||||
def make_context(self, *args, **kwargs):
|
||||
"""
|
||||
Create a context with the given arguments.
|
||||
"""
|
||||
|
||||
# Determine the context class to use
|
||||
ctxcls = RequestContext
|
||||
if 'context_class' in self.options:
|
||||
ctxcls = utils.import_class(self.options['context_class'])
|
||||
|
||||
return ctxcls(*args, **kwargs)
|
||||
|
||||
def process_request(self, req):
|
||||
"""
|
||||
Extract any authentication information in the request and
|
||||
construct an appropriate context from it.
|
||||
"""
|
||||
# Use the default empty context, with admin turned on for
|
||||
# backwards compatibility
|
||||
req.context = self.make_context(is_admin=True)
|
||||
|
||||
|
||||
def filter_factory(global_conf, **local_conf):
|
||||
"""
|
||||
Factory method for paste.deploy
|
||||
"""
|
||||
conf = global_conf.copy()
|
||||
conf.update(local_conf)
|
||||
|
||||
def filter(app):
|
||||
return ContextMiddleware(app, conf)
|
||||
|
||||
return filter
|
@ -26,33 +26,33 @@ from glance.registry import client
|
||||
logger = logging.getLogger('glance.registry')
|
||||
|
||||
|
||||
def get_registry_client(options):
|
||||
def get_registry_client(options, cxt):
|
||||
host = options['registry_host']
|
||||
port = int(options['registry_port'])
|
||||
return client.RegistryClient(host, port)
|
||||
return client.RegistryClient(host, port, auth_tok=cxt.auth_tok)
|
||||
|
||||
|
||||
def get_images_list(options, **kwargs):
|
||||
c = get_registry_client(options)
|
||||
def get_images_list(options, context, **kwargs):
|
||||
c = get_registry_client(options, context)
|
||||
return c.get_images(**kwargs)
|
||||
|
||||
|
||||
def get_images_detail(options, **kwargs):
|
||||
c = get_registry_client(options)
|
||||
def get_images_detail(options, context, **kwargs):
|
||||
c = get_registry_client(options, context)
|
||||
return c.get_images_detailed(**kwargs)
|
||||
|
||||
|
||||
def get_image_metadata(options, image_id):
|
||||
c = get_registry_client(options)
|
||||
def get_image_metadata(options, context, image_id):
|
||||
c = get_registry_client(options, context)
|
||||
return c.get_image(image_id)
|
||||
|
||||
|
||||
def add_image_metadata(options, image_meta):
|
||||
def add_image_metadata(options, context, image_meta):
|
||||
if options['debug']:
|
||||
logger.debug("Adding image metadata...")
|
||||
_debug_print_metadata(image_meta)
|
||||
|
||||
c = get_registry_client(options)
|
||||
c = get_registry_client(options, context)
|
||||
new_image_meta = c.add_image(image_meta)
|
||||
|
||||
if options['debug']:
|
||||
@ -63,12 +63,13 @@ def add_image_metadata(options, image_meta):
|
||||
return new_image_meta
|
||||
|
||||
|
||||
def update_image_metadata(options, image_id, image_meta, purge_props=False):
|
||||
def update_image_metadata(options, context, image_id, image_meta,
|
||||
purge_props=False):
|
||||
if options['debug']:
|
||||
logger.debug("Updating image metadata for image %s...", image_id)
|
||||
_debug_print_metadata(image_meta)
|
||||
|
||||
c = get_registry_client(options)
|
||||
c = get_registry_client(options, context)
|
||||
new_image_meta = c.update_image(image_id, image_meta, purge_props)
|
||||
|
||||
if options['debug']:
|
||||
@ -79,9 +80,9 @@ def update_image_metadata(options, image_id, image_meta, purge_props=False):
|
||||
return new_image_meta
|
||||
|
||||
|
||||
def delete_image_metadata(options, image_id):
|
||||
def delete_image_metadata(options, context, image_id):
|
||||
logger.debug("Deleting image metadata for image %s...", image_id)
|
||||
c = get_registry_client(options)
|
||||
c = get_registry_client(options, context)
|
||||
return c.delete_image(image_id)
|
||||
|
||||
|
||||
|
@ -33,16 +33,17 @@ class RegistryClient(BaseClient):
|
||||
|
||||
DEFAULT_PORT = 9191
|
||||
|
||||
def __init__(self, host, port=None, use_ssl=False):
|
||||
def __init__(self, host, port=None, use_ssl=False, auth_tok=None):
|
||||
"""
|
||||
Creates a new client to a Glance Registry service.
|
||||
|
||||
:param host: The host where Glance resides
|
||||
:param port: The port where Glance resides (defaults to 9191)
|
||||
:param use_ssl: Should we use HTTPS? (defaults to False)
|
||||
:param auth_tok: The auth token to pass to the server
|
||||
"""
|
||||
port = port or self.DEFAULT_PORT
|
||||
super(RegistryClient, self).__init__(host, port, use_ssl)
|
||||
super(RegistryClient, self).__init__(host, port, use_ssl, auth_tok)
|
||||
|
||||
def get_images(self, **kwargs):
|
||||
"""
|
||||
|
@ -46,12 +46,14 @@ BASE_MODEL_ATTRS = set(['id', 'created_at', 'updated_at', 'deleted_at',
|
||||
|
||||
IMAGE_ATTRS = BASE_MODEL_ATTRS | set(['name', 'status', 'size',
|
||||
'disk_format', 'container_format',
|
||||
'is_public', 'location', 'checksum'])
|
||||
'is_public', 'location', 'checksum',
|
||||
'owner'])
|
||||
|
||||
CONTAINER_FORMATS = ['ami', 'ari', 'aki', 'bare', 'ovf']
|
||||
DISK_FORMATS = ['ami', 'ari', 'aki', 'vhd', 'vmdk', 'raw', 'qcow2', 'vdi',
|
||||
'iso']
|
||||
STATUSES = ['active', 'saving', 'queued', 'killed']
|
||||
STATUSES = ['active', 'saving', 'queued', 'killed', 'pending_delete',
|
||||
'deleted']
|
||||
|
||||
|
||||
def configure_db(options):
|
||||
@ -120,7 +122,7 @@ def image_get(context, image_id, session=None):
|
||||
"""Get an image or raise if it does not exist."""
|
||||
session = session or get_session()
|
||||
try:
|
||||
return session.query(models.Image).\
|
||||
image = session.query(models.Image).\
|
||||
options(joinedload(models.Image.properties)).\
|
||||
filter_by(deleted=_deleted(context)).\
|
||||
filter_by(id=image_id).\
|
||||
@ -128,6 +130,35 @@ def image_get(context, image_id, session=None):
|
||||
except exc.NoResultFound:
|
||||
raise exception.NotFound("No image found with ID %s" % image_id)
|
||||
|
||||
# Make sure they can look at it
|
||||
if not context.is_image_visible(image):
|
||||
raise exception.NotAuthorized("Image not visible to you")
|
||||
|
||||
return image
|
||||
|
||||
|
||||
def image_get_all_pending_delete(context, delete_time=None, limit=None):
|
||||
"""Get all images that are pending deletion
|
||||
|
||||
:param limit: maximum number of images to return
|
||||
"""
|
||||
session = get_session()
|
||||
query = session.query(models.Image).\
|
||||
options(joinedload(models.Image.properties)).\
|
||||
filter_by(deleted=True).\
|
||||
filter(models.Image.status == 'pending_delete')
|
||||
|
||||
if delete_time:
|
||||
query = query.filter(models.Image.deleted_at <= delete_time)
|
||||
|
||||
query = query.order_by(desc(models.Image.deleted_at)).\
|
||||
order_by(desc(models.Image.id))
|
||||
|
||||
if limit:
|
||||
query = query.limit(limit)
|
||||
|
||||
return query.all()
|
||||
|
||||
|
||||
def image_get_all(context, filters=None, marker=None, limit=None,
|
||||
sort_key='created_at', sort_dir='desc'):
|
||||
@ -168,6 +199,13 @@ def image_get_all(context, filters=None, marker=None, limit=None,
|
||||
query = query.filter(models.Image.size <= filters['size_max'])
|
||||
del filters['size_max']
|
||||
|
||||
if 'is_public' in filters and filters['is_public'] is not None:
|
||||
the_filter = models.Image.is_public == filters['is_public']
|
||||
if filters['is_public'] and context.owner is not None:
|
||||
the_filter = or_(the_filter, models.Image.owner == context.owner)
|
||||
query = query.filter(the_filter)
|
||||
del filters['is_public']
|
||||
|
||||
for (k, v) in filters.pop('properties', {}).items():
|
||||
query = query.filter(models.Image.properties.any(name=k, value=v))
|
||||
|
||||
@ -355,6 +393,8 @@ def _deleted(context):
|
||||
Calculates whether to include deleted objects based on context.
|
||||
Currently just looks for a flag called deleted in the context dict.
|
||||
"""
|
||||
if hasattr(context, 'show_deleted'):
|
||||
return context.show_deleted
|
||||
if not hasattr(context, 'get'):
|
||||
return False
|
||||
return context.get('deleted', False)
|
||||
|
82
glance/registry/db/migrate_repo/versions/007_add_owner.py
Normal file
82
glance/registry/db/migrate_repo/versions/007_add_owner.py
Normal file
@ -0,0 +1,82 @@
|
||||
# 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.
|
||||
|
||||
from migrate.changeset import *
|
||||
from sqlalchemy import *
|
||||
from sqlalchemy.sql import and_, not_
|
||||
|
||||
from glance.registry.db.migrate_repo.schema import (
|
||||
Boolean, DateTime, BigInteger, Integer, String,
|
||||
Text, from_migration_import)
|
||||
|
||||
|
||||
def get_images_table(meta):
|
||||
"""
|
||||
Returns the Table object for the images table that corresponds to
|
||||
the images table definition of this version.
|
||||
"""
|
||||
images = Table('images', meta,
|
||||
Column('id', Integer(), primary_key=True, nullable=False),
|
||||
Column('name', String(255)),
|
||||
Column('disk_format', String(20)),
|
||||
Column('container_format', String(20)),
|
||||
Column('size', BigInteger()),
|
||||
Column('status', String(30), nullable=False),
|
||||
Column('is_public', Boolean(), nullable=False, default=False,
|
||||
index=True),
|
||||
Column('location', Text()),
|
||||
Column('created_at', DateTime(), nullable=False),
|
||||
Column('updated_at', DateTime()),
|
||||
Column('deleted_at', DateTime()),
|
||||
Column('deleted', Boolean(), nullable=False, default=False,
|
||||
index=True),
|
||||
Column('checksum', String(32)),
|
||||
Column('owner', String(255)),
|
||||
mysql_engine='InnoDB',
|
||||
useexisting=True)
|
||||
|
||||
return images
|
||||
|
||||
|
||||
def get_image_properties_table(meta):
|
||||
"""
|
||||
No changes to the image properties table from 006...
|
||||
"""
|
||||
(get_image_properties_table,) = from_migration_import(
|
||||
'006_key_to_name', ['get_image_properties_table'])
|
||||
|
||||
image_properties = get_image_properties_table(meta)
|
||||
return image_properties
|
||||
|
||||
|
||||
def upgrade(migrate_engine):
|
||||
meta = MetaData()
|
||||
meta.bind = migrate_engine
|
||||
|
||||
images = get_images_table(meta)
|
||||
|
||||
owner = Column('owner', String(255))
|
||||
owner.create(images)
|
||||
|
||||
|
||||
def downgrade(migrate_engine):
|
||||
meta = MetaData()
|
||||
meta.bind = migrate_engine
|
||||
|
||||
images = get_images_table(meta)
|
||||
|
||||
images.columns['owner'].drop()
|
@ -105,6 +105,7 @@ class Image(BASE, ModelBase):
|
||||
is_public = Column(Boolean, nullable=False, default=False)
|
||||
location = Column(Text)
|
||||
checksum = Column(String(32))
|
||||
owner = Column(String(255))
|
||||
|
||||
|
||||
class ImageProperty(BASE, ModelBase):
|
||||
|
@ -61,7 +61,7 @@ class Controller(object):
|
||||
Get images, wrapping in exception if necessary.
|
||||
"""
|
||||
try:
|
||||
return db_api.image_get_all(None, **params)
|
||||
return db_api.image_get_all(context, **params)
|
||||
except exception.NotFound, e:
|
||||
msg = "Invalid marker. Image could not be found."
|
||||
raise exc.HTTPBadRequest(explanation=msg)
|
||||
@ -87,7 +87,7 @@ class Controller(object):
|
||||
}
|
||||
"""
|
||||
params = self._get_query_params(req)
|
||||
images = self._get_images(None, **params)
|
||||
images = self._get_images(req.context, **params)
|
||||
|
||||
results = []
|
||||
for image in images:
|
||||
@ -111,7 +111,7 @@ class Controller(object):
|
||||
"""
|
||||
params = self._get_query_params(req)
|
||||
|
||||
images = self._get_images(None, **params)
|
||||
images = self._get_images(req.context, **params)
|
||||
image_dicts = [make_image_dict(i) for i in images]
|
||||
return dict(images=image_dicts)
|
||||
|
||||
@ -146,7 +146,11 @@ class Controller(object):
|
||||
filters = {}
|
||||
properties = {}
|
||||
|
||||
filters['is_public'] = self._get_is_public(req)
|
||||
if req.context.is_admin:
|
||||
# Only admin gets to look for non-public images
|
||||
filters['is_public'] = self._get_is_public(req)
|
||||
else:
|
||||
filters['is_public'] = True
|
||||
for param in req.str_params:
|
||||
if param in SUPPORTED_FILTERS:
|
||||
filters[param] = req.str_params.get(param)
|
||||
@ -223,9 +227,15 @@ class Controller(object):
|
||||
def show(self, req, id):
|
||||
"""Return data about the given image id."""
|
||||
try:
|
||||
image = db_api.image_get(None, id)
|
||||
image = db_api.image_get(req.context, id)
|
||||
except exception.NotFound:
|
||||
raise exc.HTTPNotFound()
|
||||
except exception.NotAuthorized:
|
||||
# If it's private and doesn't belong to them, don't let on
|
||||
# that it exists
|
||||
logger.info("Access by %s to image %s denied" %
|
||||
(req.context.user, id))
|
||||
raise exc.HTTPNotFound()
|
||||
|
||||
return dict(image=make_image_dict(image))
|
||||
|
||||
@ -238,11 +248,19 @@ class Controller(object):
|
||||
|
||||
:retval Returns 200 if delete was successful, a fault if not.
|
||||
"""
|
||||
context = None
|
||||
if req.context.read_only:
|
||||
raise exc.HTTPForbidden()
|
||||
|
||||
try:
|
||||
db_api.image_destroy(context, id)
|
||||
db_api.image_destroy(req.context, id)
|
||||
except exception.NotFound:
|
||||
return exc.HTTPNotFound()
|
||||
except exception.NotAuthorized:
|
||||
# If it's private and doesn't belong to them, don't let on
|
||||
# that it exists
|
||||
logger.info("Access by %s to image %s denied" %
|
||||
(req.context.user, id))
|
||||
raise exc.HTTPNotFound()
|
||||
|
||||
def create(self, req, body):
|
||||
"""
|
||||
@ -255,14 +273,20 @@ class Controller(object):
|
||||
which will include the newly-created image's internal id
|
||||
in the 'id' field
|
||||
"""
|
||||
if req.context.read_only:
|
||||
raise exc.HTTPForbidden()
|
||||
|
||||
image_data = body['image']
|
||||
|
||||
# Ensure the image has a status set
|
||||
image_data.setdefault('status', 'active')
|
||||
|
||||
context = None
|
||||
# Set up the image owner
|
||||
if not req.context.is_admin or 'owner' not in image_data:
|
||||
image_data['owner'] = req.context.owner
|
||||
|
||||
try:
|
||||
image_data = db_api.image_create(context, image_data)
|
||||
image_data = db_api.image_create(req.context, image_data)
|
||||
return dict(image=make_image_dict(image_data))
|
||||
except exception.Duplicate:
|
||||
msg = ("Image with identifier %s already exists!" % id)
|
||||
@ -283,18 +307,25 @@ class Controller(object):
|
||||
|
||||
:retval Returns the updated image information as a mapping,
|
||||
"""
|
||||
if req.context.read_only:
|
||||
raise exc.HTTPForbidden()
|
||||
|
||||
image_data = body['image']
|
||||
|
||||
# Prohibit modification of 'owner'
|
||||
if not req.context.is_admin and 'owner' in image_data:
|
||||
del image_data['owner']
|
||||
|
||||
purge_props = req.headers.get("X-Glance-Registry-Purge-Props", "false")
|
||||
context = None
|
||||
try:
|
||||
logger.debug("Updating image %(id)s with metadata: %(image_data)r"
|
||||
% locals())
|
||||
if purge_props == "true":
|
||||
updated_image = db_api.image_update(context, id, image_data,
|
||||
True)
|
||||
updated_image = db_api.image_update(req.context, id,
|
||||
image_data, True)
|
||||
else:
|
||||
updated_image = db_api.image_update(context, id, image_data)
|
||||
updated_image = db_api.image_update(req.context, id,
|
||||
image_data)
|
||||
return dict(image=make_image_dict(updated_image))
|
||||
except exception.Invalid, e:
|
||||
msg = ("Failed to update image metadata. "
|
||||
@ -305,6 +336,14 @@ class Controller(object):
|
||||
raise exc.HTTPNotFound(body='Image not found',
|
||||
request=req,
|
||||
content_type='text/plain')
|
||||
except exception.NotAuthorized:
|
||||
# If it's private and doesn't belong to them, don't let on
|
||||
# that it exists
|
||||
logger.info("Access by %s to image %s denied" %
|
||||
(req.context.user, id))
|
||||
raise exc.HTTPNotFound(body='Image not found',
|
||||
request=req,
|
||||
content_type='text/plain')
|
||||
|
||||
|
||||
def create_resource(options):
|
||||
|
@ -15,14 +15,19 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import logging
|
||||
import optparse
|
||||
import os
|
||||
import urlparse
|
||||
|
||||
from glance.common import exception
|
||||
from glance import registry
|
||||
from glance.common import config, exception
|
||||
from glance.store import location
|
||||
|
||||
|
||||
logger = logging.getLogger('glance.store')
|
||||
|
||||
|
||||
# TODO(sirp): should this be moved out to common/utils.py ?
|
||||
def _file_iter(f, size):
|
||||
"""
|
||||
@ -101,3 +106,22 @@ def get_store_from_location(uri):
|
||||
"""
|
||||
loc = location.get_location_from_uri(uri)
|
||||
return loc.store_name
|
||||
|
||||
|
||||
def schedule_delete_from_backend(uri, options, context, id, **kwargs):
|
||||
"""
|
||||
Given a uri and a time, schedule the deletion of an image.
|
||||
"""
|
||||
use_delay = config.get_option(options, 'delayed_delete', type='bool',
|
||||
default=False)
|
||||
if not use_delay:
|
||||
registry.update_image_metadata(options, context, id,
|
||||
{'status': 'deleted'})
|
||||
try:
|
||||
return delete_from_backend(uri, **kwargs)
|
||||
except (UnsupportedBackend, exception.NotFound):
|
||||
msg = "Failed to delete image from store (%s). "
|
||||
logger.error(msg % uri)
|
||||
|
||||
registry.update_image_metadata(options, context, id,
|
||||
{'status': 'pending_delete'})
|
||||
|
@ -18,6 +18,7 @@
|
||||
"""Storage backend for S3 or Storage Servers that follow the S3 Protocol"""
|
||||
|
||||
import logging
|
||||
import httplib
|
||||
import urlparse
|
||||
|
||||
from glance.common import exception
|
||||
@ -115,7 +116,7 @@ class StoreLocation(glance.store.location.StoreLocation):
|
||||
self.key = path_parts.pop()
|
||||
self.bucket = path_parts.pop()
|
||||
if len(path_parts) > 0:
|
||||
self.s3serviceurl = '/'.join(path_parts)
|
||||
self.s3serviceurl = '/'.join(path_parts).strip('/')
|
||||
else:
|
||||
reason = "Badly formed S3 URI. Missing s3 service URL."
|
||||
raise exception.BadStoreUri(uri, reason)
|
||||
@ -183,7 +184,8 @@ class S3Backend(glance.store.Backend):
|
||||
from boto.s3.connection import S3Connection
|
||||
|
||||
s3_conn = S3Connection(loc.accesskey, loc.secretkey,
|
||||
host=loc.s3serviceurl)
|
||||
host=loc.s3serviceurl,
|
||||
is_secure=(loc.scheme == 's3+https'))
|
||||
bucket_obj = get_bucket(s3_conn, loc.bucket)
|
||||
|
||||
key = get_key(bucket_obj, loc.key)
|
||||
@ -258,7 +260,9 @@ class S3Backend(glance.store.Backend):
|
||||
'accesskey': access_key,
|
||||
'secretkey': secret_key})
|
||||
|
||||
s3_conn = S3Connection(access_key, secret_key, host=loc.s3serviceurl)
|
||||
s3_conn = S3Connection(loc.accesskey, loc.secretkey,
|
||||
host=loc.s3serviceurl,
|
||||
is_secure=(loc.scheme == 's3+https'))
|
||||
|
||||
create_bucket_if_missing(bucket, s3_conn, options)
|
||||
|
||||
@ -294,7 +298,8 @@ class S3Backend(glance.store.Backend):
|
||||
loc = location.store_location
|
||||
from boto.s3.connection import S3Connection
|
||||
s3_conn = S3Connection(loc.accesskey, loc.secretkey,
|
||||
host=loc.s3serviceurl)
|
||||
host=loc.s3serviceurl,
|
||||
is_secure=(loc.scheme == 's3+https'))
|
||||
bucket_obj = get_bucket(s3_conn, loc.bucket)
|
||||
|
||||
# Close the key when we're through.
|
||||
|
90
glance/store/scrubber.py
Normal file
90
glance/store/scrubber.py
Normal file
@ -0,0 +1,90 @@
|
||||
# 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.
|
||||
|
||||
import datetime
|
||||
import eventlet
|
||||
import logging
|
||||
|
||||
from glance import registry
|
||||
from glance import store
|
||||
from glance.common import config
|
||||
from glance.common import context
|
||||
from glance.common import exception
|
||||
from glance.registry.db import api as db_api
|
||||
|
||||
|
||||
logger = logging.getLogger('glance.store.scrubber')
|
||||
|
||||
|
||||
class Daemon(object):
|
||||
def __init__(self, wakeup_time=300, threads=1000):
|
||||
logger.info("Starting Daemon: " +
|
||||
"wakeup_time=%s threads=%s" % (wakeup_time, threads))
|
||||
self.wakeup_time = wakeup_time
|
||||
self.event = eventlet.event.Event()
|
||||
self.pool = eventlet.greenpool.GreenPool(threads)
|
||||
|
||||
def start(self, application):
|
||||
self._run(application)
|
||||
|
||||
def wait(self):
|
||||
try:
|
||||
self.event.wait()
|
||||
except KeyboardInterrupt:
|
||||
logger.info("Daemon Shutdown on KeyboardInterrupt")
|
||||
|
||||
def _run(self, application):
|
||||
logger.debug("Runing application")
|
||||
self.pool.spawn_n(application.run, self.pool, self.event)
|
||||
eventlet.spawn_after(self.wakeup_time, self._run, application)
|
||||
logger.debug("Next run scheduled in %s seconds" % self.wakeup_time)
|
||||
|
||||
|
||||
class Scrubber(object):
|
||||
def __init__(self, options):
|
||||
logger.info("Initializing scrubber with options: %s" % options)
|
||||
self.options = options
|
||||
scrub_time = config.get_option(options, 'scrub_time', type='int',
|
||||
default=0)
|
||||
logger.info("Scrub interval set to %s seconds" % scrub_time)
|
||||
self.scrub_time = datetime.timedelta(seconds=scrub_time)
|
||||
db_api.configure_db(options)
|
||||
|
||||
def run(self, pool, event=None):
|
||||
delete_time = datetime.datetime.utcnow() - self.scrub_time
|
||||
logger.info("Getting images deleted before %s" % delete_time)
|
||||
pending = db_api.image_get_all_pending_delete(None, delete_time)
|
||||
logger.info("Deleting %s images" % len(pending))
|
||||
delete_work = [(p['id'], p['location']) for p in pending]
|
||||
pool.starmap(self._delete, delete_work)
|
||||
|
||||
def _delete(self, id, location):
|
||||
try:
|
||||
logger.debug("Deleting %s" % location)
|
||||
store.delete_from_backend(location)
|
||||
except (store.UnsupportedBackend, exception.NotFound):
|
||||
msg = "Failed to delete image from store (%s). "
|
||||
logger.error(msg % uri)
|
||||
|
||||
ctx = context.RequestContext(is_admin=True, show_deleted=True)
|
||||
db_api.image_update(ctx, id, {'status': 'deleted'})
|
||||
|
||||
|
||||
def app_factory(global_config, **local_conf):
|
||||
conf = global_config.copy()
|
||||
conf.update(local_conf)
|
||||
return Scrubber(conf)
|
@ -127,7 +127,7 @@ class ApiServer(Server):
|
||||
Server object that starts/stops/manages the API server
|
||||
"""
|
||||
|
||||
def __init__(self, test_dir, port, registry_port):
|
||||
def __init__(self, test_dir, port, registry_port, delayed_delete=False):
|
||||
super(ApiServer, self).__init__(test_dir, port)
|
||||
self.server_name = 'api'
|
||||
self.default_store = 'file'
|
||||
@ -141,6 +141,7 @@ class ApiServer(Server):
|
||||
self.s3_store_access_key = ""
|
||||
self.s3_store_secret_key = ""
|
||||
self.s3_store_bucket = ""
|
||||
self.delayed_delete = delayed_delete
|
||||
self.conf_base = """[DEFAULT]
|
||||
verbose = %(verbose)s
|
||||
debug = %(debug)s
|
||||
@ -155,9 +156,10 @@ s3_store_host = %(s3_store_host)s
|
||||
s3_store_access_key = %(s3_store_access_key)s
|
||||
s3_store_secret_key = %(s3_store_secret_key)s
|
||||
s3_store_bucket = %(s3_store_bucket)s
|
||||
delayed_delete = %(delayed_delete)s
|
||||
|
||||
[pipeline:glance-api]
|
||||
pipeline = versionnegotiation apiv1app
|
||||
pipeline = versionnegotiation context apiv1app
|
||||
|
||||
[pipeline:versions]
|
||||
pipeline = versionsapp
|
||||
@ -170,6 +172,9 @@ paste.app_factory = glance.api.v1:app_factory
|
||||
|
||||
[filter:versionnegotiation]
|
||||
paste.filter_factory = glance.api.middleware.version_negotiation:filter_factory
|
||||
|
||||
[filter:context]
|
||||
paste.filter_factory = glance.common.context:filter_factory
|
||||
"""
|
||||
|
||||
|
||||
@ -199,8 +204,44 @@ log_file = %(log_file)s
|
||||
sql_connection = %(sql_connection)s
|
||||
sql_idle_timeout = 3600
|
||||
|
||||
[app:glance-registry]
|
||||
[pipeline:glance-registry]
|
||||
pipeline = context registryapp
|
||||
|
||||
[app:registryapp]
|
||||
paste.app_factory = glance.registry.server:app_factory
|
||||
|
||||
[filter:context]
|
||||
paste.filter_factory = glance.common.context:filter_factory
|
||||
"""
|
||||
|
||||
|
||||
class ScrubberDaemon(Server):
|
||||
"""
|
||||
Server object that starts/stops/manages the Scrubber server
|
||||
"""
|
||||
|
||||
def __init__(self, test_dir, sql_connection, daemon=False):
|
||||
# NOTE(jkoelker): Set the port to 0 since we actually don't listen
|
||||
super(ScrubberDaemon, self).__init__(test_dir, 0)
|
||||
self.server_name = 'scrubber'
|
||||
self.daemon = daemon
|
||||
|
||||
self.sql_connection = sql_connection
|
||||
|
||||
self.pid_file = os.path.join(self.test_dir, "scrubber.pid")
|
||||
self.log_file = os.path.join(self.test_dir, "scrubber.log")
|
||||
self.conf_base = """[DEFAULT]
|
||||
verbose = %(verbose)s
|
||||
debug = %(debug)s
|
||||
log_file = %(log_file)s
|
||||
scrub_time = 5
|
||||
daemon = %(daemon)s
|
||||
wakeup_time = 2
|
||||
sql_connection = %(sql_connection)s
|
||||
sql_idle_timeout = 3600
|
||||
|
||||
[app:glance-scrubber]
|
||||
paste.app_factory = glance.store.scrubber:app_factory
|
||||
"""
|
||||
|
||||
|
||||
@ -225,8 +266,13 @@ class FunctionalTest(unittest.TestCase):
|
||||
self.registry_server = RegistryServer(self.test_dir,
|
||||
self.registry_port)
|
||||
|
||||
registry_db = self.registry_server.sql_connection
|
||||
self.scrubber_daemon = ScrubberDaemon(self.test_dir,
|
||||
sql_connection=registry_db)
|
||||
|
||||
self.pid_files = [self.api_server.pid_file,
|
||||
self.registry_server.pid_file]
|
||||
self.registry_server.pid_file,
|
||||
self.scrubber_daemon.pid_file]
|
||||
self.files_to_destroy = []
|
||||
|
||||
def tearDown(self):
|
||||
@ -312,6 +358,13 @@ class FunctionalTest(unittest.TestCase):
|
||||
"Got: %s" % err)
|
||||
self.assertTrue("Starting glance-registry with" in out)
|
||||
|
||||
exitcode, out, err = self.scrubber_daemon.start(**kwargs)
|
||||
|
||||
self.assertEqual(0, exitcode,
|
||||
"Failed to spin up the Scrubber daemon. "
|
||||
"Got: %s" % err)
|
||||
self.assertTrue("Starting glance-scrubber with" in out)
|
||||
|
||||
self.wait_for_servers()
|
||||
|
||||
def ping_server(self, port):
|
||||
@ -369,6 +422,10 @@ class FunctionalTest(unittest.TestCase):
|
||||
"Failed to spin down the Registry server. "
|
||||
"Got: %s" % err)
|
||||
|
||||
exitcode, out, err = self.scrubber_daemon.stop()
|
||||
self.assertEqual(0, exitcode,
|
||||
"Failed to spin down the Scrubber daemon. "
|
||||
"Got: %s" % err)
|
||||
# If all went well, then just remove the test directory.
|
||||
# We only want to check the logs and stuff if something
|
||||
# went wrong...
|
||||
|
@ -229,7 +229,7 @@ class TestS3(functional.FunctionalTest):
|
||||
lines = out.split("\r\n")
|
||||
status_line = lines[0]
|
||||
|
||||
self.assertEqual("HTTP/1.1 201 Created", status_line)
|
||||
self.assertEqual("HTTP/1.1 201 Created", status_line, out)
|
||||
|
||||
# 4. HEAD /images
|
||||
# Verify image found now
|
||||
|
111
tests/functional/test_scrubber.py
Normal file
111
tests/functional/test_scrubber.py
Normal file
@ -0,0 +1,111 @@
|
||||
# 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.
|
||||
|
||||
import os
|
||||
import time
|
||||
import unittest
|
||||
|
||||
from sqlalchemy import create_engine
|
||||
|
||||
from tests import functional
|
||||
from tests.utils import execute
|
||||
|
||||
from glance import client
|
||||
|
||||
|
||||
TEST_IMAGE_DATA = '*' * 5 * 1024
|
||||
TEST_IMAGE_META = {'name': 'test_image',
|
||||
'is_public': False,
|
||||
'disk_format': 'raw',
|
||||
'container_format': 'ovf'}
|
||||
|
||||
|
||||
class TestScrubber(functional.FunctionalTest):
|
||||
|
||||
"""Test that delayed_delete works and the scrubber deletes"""
|
||||
|
||||
def _get_client(self):
|
||||
return client.Client("localhost", self.api_port)
|
||||
|
||||
@functional.runs_sql
|
||||
def test_immediate_delete(self):
|
||||
"""
|
||||
test that images get deleted immediately by default
|
||||
"""
|
||||
|
||||
self.cleanup()
|
||||
self.start_servers()
|
||||
|
||||
client = self._get_client()
|
||||
meta = client.add_image(TEST_IMAGE_META, TEST_IMAGE_DATA)
|
||||
id = meta['id']
|
||||
|
||||
sql = "SELECT * FROM images WHERE status = 'pending_delete'"
|
||||
recs = list(self.run_sql_cmd(sql))
|
||||
self.assertFalse(recs)
|
||||
|
||||
client.delete_image(id)
|
||||
recs = list(self.run_sql_cmd(sql))
|
||||
self.assertFalse(recs)
|
||||
|
||||
sql = "SELECT * FROM images WHERE id = '%s'" % id
|
||||
recs = list(self.run_sql_cmd(sql))
|
||||
self.assertTrue(recs)
|
||||
for rec in recs:
|
||||
self.assertEqual(rec['status'], 'deleted')
|
||||
|
||||
self.stop_servers()
|
||||
|
||||
@functional.runs_sql
|
||||
def test_delayed_delete(self):
|
||||
"""
|
||||
test that images don't get deleted immediatly and that the scrubber
|
||||
scrubs them
|
||||
"""
|
||||
|
||||
self.cleanup()
|
||||
registry_db = self.registry_server.sql_connection
|
||||
self.start_servers(delayed_delete=True, sql_connection=registry_db,
|
||||
daemon=True)
|
||||
|
||||
client = self._get_client()
|
||||
meta = client.add_image(TEST_IMAGE_META, TEST_IMAGE_DATA)
|
||||
id = meta['id']
|
||||
|
||||
sql = "SELECT * FROM images WHERE status = 'pending_delete'"
|
||||
recs = list(self.run_sql_cmd(sql))
|
||||
self.assertFalse(recs)
|
||||
|
||||
client.delete_image(id)
|
||||
recs = self.run_sql_cmd(sql)
|
||||
self.assertTrue(recs)
|
||||
|
||||
sql = "SELECT * FROM images WHERE id = '%s'" % id
|
||||
recs = list(self.run_sql_cmd(sql))
|
||||
self.assertTrue(recs)
|
||||
for rec in recs:
|
||||
self.assertEqual(rec['status'], 'pending_delete')
|
||||
|
||||
# Wait 15 seconds for the scrubber to scrub
|
||||
time.sleep(15)
|
||||
|
||||
recs = list(self.run_sql_cmd(sql))
|
||||
self.assertTrue(recs)
|
||||
for rec in recs:
|
||||
self.assertEqual(rec['status'], 'deleted')
|
||||
|
||||
self.stop_servers()
|
@ -29,6 +29,7 @@ import stubout
|
||||
import webob
|
||||
|
||||
import glance.common.client
|
||||
from glance.common import context
|
||||
from glance.common import exception
|
||||
from glance.registry import server as rserver
|
||||
from glance.api import v1 as server
|
||||
@ -172,7 +173,8 @@ def stub_out_registry_and_store_server(stubs):
|
||||
"sqlite://")
|
||||
options = {'sql_connection': sql_connection, 'verbose': VERBOSE,
|
||||
'debug': DEBUG}
|
||||
res = self.req.get_response(rserver.API(options))
|
||||
api = context.ContextMiddleware(rserver.API(options), options)
|
||||
res = self.req.get_response(api)
|
||||
|
||||
# httplib.Response has a read() method...fake it out
|
||||
def fake_reader():
|
||||
@ -223,7 +225,8 @@ def stub_out_registry_and_store_server(stubs):
|
||||
'registry_port': '9191',
|
||||
'default_store': 'file',
|
||||
'filesystem_store_datadir': FAKE_FILESYSTEM_ROOTDIR}
|
||||
res = self.req.get_response(server.API(options))
|
||||
api = context.ContextMiddleware(server.API(options), options)
|
||||
res = self.req.get_response(api)
|
||||
|
||||
# httplib.Response has a read() method...fake it out
|
||||
def fake_reader():
|
||||
@ -303,6 +306,7 @@ def stub_out_registry_db_image_api(stubs):
|
||||
|
||||
def __init__(self):
|
||||
self.images = FakeDatastore.FIXTURES
|
||||
self.deleted_images = []
|
||||
self.next_id = 3
|
||||
|
||||
def image_create(self, _context, values):
|
||||
@ -375,6 +379,8 @@ def stub_out_registry_db_image_api(stubs):
|
||||
def image_destroy(self, _context, image_id):
|
||||
image = self.image_get(_context, image_id)
|
||||
self.images.remove(image)
|
||||
image['deleted_at'] = datetime.datetime.utcnow()
|
||||
self.deleted_images.append(image)
|
||||
|
||||
def image_get(self, _context, image_id):
|
||||
|
||||
@ -386,6 +392,13 @@ def stub_out_registry_db_image_api(stubs):
|
||||
else:
|
||||
return images[0]
|
||||
|
||||
def image_get_all_pending_delete(self, _context, delete_time=None,
|
||||
limit=None):
|
||||
images = [f for f in self.deleted_images \
|
||||
if f['status'] == 'pending_delete' and \
|
||||
f['deleted_at'] <= delete_time]
|
||||
return images
|
||||
|
||||
def image_get_all(self, _context, filters=None, marker=None,
|
||||
limit=1000, sort_key=None, sort_dir=None):
|
||||
images = self.images
|
||||
@ -458,5 +471,7 @@ def stub_out_registry_db_image_api(stubs):
|
||||
fake_datastore.image_destroy)
|
||||
stubs.Set(glance.registry.db.api, 'image_get',
|
||||
fake_datastore.image_get)
|
||||
stubs.Set(glance.registry.db.api, 'image_get_all_pending_delete',
|
||||
fake_datastore.image_get_all_pending_delete)
|
||||
stubs.Set(glance.registry.db.api, 'image_get_all',
|
||||
fake_datastore.image_get_all)
|
||||
|
@ -26,6 +26,7 @@ import stubout
|
||||
import webob
|
||||
|
||||
from glance.api import v1 as server
|
||||
from glance.common import context
|
||||
from glance.registry import server as rserver
|
||||
import glance.registry.db.api
|
||||
from tests import stubs
|
||||
@ -41,8 +42,9 @@ class TestRegistryAPI(unittest.TestCase):
|
||||
stubs.stub_out_registry_and_store_server(self.stubs)
|
||||
stubs.stub_out_registry_db_image_api(self.stubs)
|
||||
stubs.stub_out_filesystem_backend()
|
||||
self.api = rserver.API({'verbose': VERBOSE,
|
||||
'debug': DEBUG})
|
||||
options = {'verbose': VERBOSE,
|
||||
'debug': DEBUG}
|
||||
self.api = context.ContextMiddleware(rserver.API(options), options)
|
||||
|
||||
def tearDown(self):
|
||||
"""Clear the test environment"""
|
||||
@ -1458,7 +1460,7 @@ class TestGlanceAPI(unittest.TestCase):
|
||||
'sql_connection': sql_connection,
|
||||
'default_store': 'file',
|
||||
'filesystem_store_datadir': stubs.FAKE_FILESYSTEM_ROOTDIR}
|
||||
self.api = server.API(options)
|
||||
self.api = context.ContextMiddleware(server.API(options), options)
|
||||
|
||||
def tearDown(self):
|
||||
"""Clear the test environment"""
|
||||
|
148
tests/unit/test_context.py
Normal file
148
tests/unit/test_context.py
Normal file
@ -0,0 +1,148 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2010-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.
|
||||
|
||||
import unittest
|
||||
|
||||
from glance.common import context
|
||||
|
||||
|
||||
class FakeImage(object):
|
||||
"""
|
||||
Fake image for providing the image attributes needed for
|
||||
TestContext.
|
||||
"""
|
||||
|
||||
def __init__(self, owner, is_public):
|
||||
self.owner = owner
|
||||
self.is_public = is_public
|
||||
|
||||
|
||||
class TestContext(unittest.TestCase):
|
||||
def do_visible(self, exp_res, img_owner, img_public, **kwargs):
|
||||
"""
|
||||
Perform a context test. Creates a (fake) image with the
|
||||
specified owner and is_public attributes, then creates a
|
||||
context with the given keyword arguments and expects exp_res
|
||||
as the result of an is_image_visible() call on the context.
|
||||
"""
|
||||
|
||||
img = FakeImage(img_owner, img_public)
|
||||
ctx = context.RequestContext(**kwargs)
|
||||
|
||||
self.assertEqual(ctx.is_image_visible(img), exp_res)
|
||||
|
||||
def test_empty_public(self):
|
||||
"""
|
||||
Tests that an empty context (with is_admin set to True) can
|
||||
access an image with is_public set to True.
|
||||
"""
|
||||
self.do_visible(True, None, True, is_admin=True)
|
||||
|
||||
def test_empty_public_owned(self):
|
||||
"""
|
||||
Tests that an empty context (with is_admin set to True) can
|
||||
access an owned image with is_public set to True.
|
||||
"""
|
||||
self.do_visible(True, 'pattieblack', True, is_admin=True)
|
||||
|
||||
def test_empty_private(self):
|
||||
"""
|
||||
Tests that an empty context (with is_admin set to True) can
|
||||
access an image with is_public set to False.
|
||||
"""
|
||||
self.do_visible(True, None, False, is_admin=True)
|
||||
|
||||
def test_empty_private_owned(self):
|
||||
"""
|
||||
Tests that an empty context (with is_admin set to True) can
|
||||
access an owned image with is_public set to False.
|
||||
"""
|
||||
self.do_visible(True, 'pattieblack', False, is_admin=True)
|
||||
|
||||
def test_anon_public(self):
|
||||
"""
|
||||
Tests that an anonymous context (with is_admin set to False)
|
||||
can access an image with is_public set to True.
|
||||
"""
|
||||
self.do_visible(True, None, True)
|
||||
|
||||
def test_anon_public_owned(self):
|
||||
"""
|
||||
Tests that an anonymous context (with is_admin set to False)
|
||||
can access an owned image with is_public set to True.
|
||||
"""
|
||||
self.do_visible(True, 'pattieblack', True)
|
||||
|
||||
def test_anon_private(self):
|
||||
"""
|
||||
Tests that an anonymous context (with is_admin set to False)
|
||||
can access an unowned image with is_public set to False.
|
||||
"""
|
||||
self.do_visible(True, None, False)
|
||||
|
||||
def test_anon_private_owned(self):
|
||||
"""
|
||||
Tests that an anonymous context (with is_admin set to False)
|
||||
cannot access an owned image with is_public set to False.
|
||||
"""
|
||||
self.do_visible(False, 'pattieblack', False)
|
||||
|
||||
def test_auth_public(self):
|
||||
"""
|
||||
Tests that an authenticated context (with is_admin set to
|
||||
False) can access an image with is_public set to True.
|
||||
"""
|
||||
self.do_visible(True, None, True, tenant='froggy')
|
||||
|
||||
def test_auth_public_unowned(self):
|
||||
"""
|
||||
Tests that an authenticated context (with is_admin set to
|
||||
False) can access an image (which it does not own) with
|
||||
is_public set to True.
|
||||
"""
|
||||
self.do_visible(True, 'pattieblack', True, tenant='froggy')
|
||||
|
||||
def test_auth_public_owned(self):
|
||||
"""
|
||||
Tests that an authenticated context (with is_admin set to
|
||||
False) can access an image (which it does own) with is_public
|
||||
set to True.
|
||||
"""
|
||||
self.do_visible(True, 'pattieblack', True, tenant='pattieblack')
|
||||
|
||||
def test_auth_private(self):
|
||||
"""
|
||||
Tests that an authenticated context (with is_admin set to
|
||||
False) can access an image with is_public set to False.
|
||||
"""
|
||||
self.do_visible(True, None, False, tenant='froggy')
|
||||
|
||||
def test_auth_private_unowned(self):
|
||||
"""
|
||||
Tests that an authenticated context (with is_admin set to
|
||||
False) cannot access an image (which it does not own) with
|
||||
is_public set to False.
|
||||
"""
|
||||
self.do_visible(False, 'pattieblack', False, tenant='froggy')
|
||||
|
||||
def test_auth_private_owned(self):
|
||||
"""
|
||||
Tests that an authenticated context (with is_admin set to
|
||||
False) can access an image (which it does own) with is_public
|
||||
set to False.
|
||||
"""
|
||||
self.do_visible(True, 'pattieblack', False, tenant='pattieblack')
|
@ -28,7 +28,7 @@ import stubout
|
||||
import boto.s3.connection
|
||||
|
||||
from glance.common import exception
|
||||
from glance.store import BackendException
|
||||
from glance.store import BackendException, UnsupportedBackend
|
||||
from glance.store.location import get_location_from_uri
|
||||
from glance.store.s3 import S3Backend
|
||||
|
||||
@ -119,7 +119,9 @@ def stub_out_s3(stubs):
|
||||
k.set_contents_from_file(StringIO.StringIO("*" * FIVE_KB))
|
||||
|
||||
def fake_connection_constructor(self, *args, **kwargs):
|
||||
pass
|
||||
host = kwargs.get('host')
|
||||
if host.startswith('http://') or host.startswith('https://'):
|
||||
raise UnsupportedBackend(host)
|
||||
|
||||
def fake_get_bucket(conn, bucket_id):
|
||||
bucket = fixture_buckets.get(bucket_id)
|
||||
|
Loading…
x
Reference in New Issue
Block a user