Fixed review stuff from Brian

This commit is contained in:
jaypipes@gmail.com 2011-07-25 13:45:35 -04:00
commit 684d89117e
27 changed files with 1053 additions and 76 deletions

@ -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

@ -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

@ -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

@ -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)

@ -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

@ -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

@ -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

@ -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)