diff --git a/etc/glance-api-paste.ini b/etc/glance-api-paste.ini index 687902743a..a3caddb3c8 100644 --- a/etc/glance-api-paste.ini +++ b/etc/glance-api-paste.ini @@ -37,15 +37,11 @@ pipeline = cors healthcheck http_proxy_to_wsgi versionnegotiation osprofiler con [composite:rootapp] paste.composite_factory = glance.api:root_app_factory /: apiversions -/v1: apiv1app /v2: apiv2app [app:apiversions] paste.app_factory = glance.api.versions:create_resource -[app:apiv1app] -paste.app_factory = glance.api.v1.router:API.factory - [app:apiv2app] paste.app_factory = glance.api.v2.router:API.factory diff --git a/glance/api/cached_images.py b/glance/api/cached_images.py deleted file mode 100644 index 04d1c0ce40..0000000000 --- a/glance/api/cached_images.py +++ /dev/null @@ -1,129 +0,0 @@ -# Copyright 2011 OpenStack Foundation -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -""" -Controller for Image Cache Management API -""" - -from oslo_log import log as logging -import webob.exc - -from glance.api import policy -from glance.api.v1 import controller -from glance.common import exception -from glance.common import wsgi -from glance import image_cache - -LOG = logging.getLogger(__name__) - - -class Controller(controller.BaseController): - """ - A controller for managing cached images. - """ - - def __init__(self): - self.cache = image_cache.ImageCache() - self.policy = policy.Enforcer() - - def _enforce(self, req): - """Authorize request against 'manage_image_cache' policy""" - try: - self.policy.enforce(req.context, 'manage_image_cache', {}) - except exception.Forbidden: - LOG.debug("User not permitted to manage the image cache") - raise webob.exc.HTTPForbidden() - - def get_cached_images(self, req): - """ - GET /cached_images - - Returns a mapping of records about cached images. - """ - self._enforce(req) - images = self.cache.get_cached_images() - return dict(cached_images=images) - - def delete_cached_image(self, req, image_id): - """ - DELETE /cached_images/ - - Removes an image from the cache. - """ - self._enforce(req) - self.cache.delete_cached_image(image_id) - - def delete_cached_images(self, req): - """ - DELETE /cached_images - Clear all active cached images - - Removes all images from the cache. - """ - self._enforce(req) - return dict(num_deleted=self.cache.delete_all_cached_images()) - - def get_queued_images(self, req): - """ - GET /queued_images - - Returns a mapping of records about queued images. - """ - self._enforce(req) - images = self.cache.get_queued_images() - return dict(queued_images=images) - - def queue_image(self, req, image_id): - """ - PUT /queued_images/ - - Queues an image for caching. We do not check to see if - the image is in the registry here. That is done by the - prefetcher... - """ - self._enforce(req) - self.cache.queue_image(image_id) - - def delete_queued_image(self, req, image_id): - """ - DELETE /queued_images/ - - Removes an image from the cache. - """ - self._enforce(req) - self.cache.delete_queued_image(image_id) - - def delete_queued_images(self, req): - """ - DELETE /queued_images - Clear all active queued images - - Removes all images from the cache. - """ - self._enforce(req) - return dict(num_deleted=self.cache.delete_all_queued_images()) - - -class CachedImageDeserializer(wsgi.JSONRequestDeserializer): - pass - - -class CachedImageSerializer(wsgi.JSONResponseSerializer): - pass - - -def create_resource(): - """Cached Images resource factory method""" - deserializer = CachedImageDeserializer() - serializer = CachedImageSerializer() - return wsgi.Resource(Controller(), deserializer, serializer) diff --git a/glance/api/v1/__init__.py b/glance/api/v1/__init__.py index aa3d871848..e69de29bb2 100644 --- a/glance/api/v1/__init__.py +++ b/glance/api/v1/__init__.py @@ -1,26 +0,0 @@ -# Copyright 2011 OpenStack Foundation -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -SUPPORTED_FILTERS = ['name', 'status', 'container_format', 'disk_format', - 'min_ram', 'min_disk', 'size_min', 'size_max', - 'is_public', 'changes-since', 'protected'] - -SUPPORTED_PARAMS = ('limit', 'marker', 'sort_key', 'sort_dir') - -# Metadata which only an admin can change once the image is active -ACTIVE_IMMUTABLE = ('size', 'checksum') - -# Metadata which cannot be changed (irrespective of the current image state) -IMMUTABLE = ('status', 'id') diff --git a/glance/api/v1/controller.py b/glance/api/v1/controller.py deleted file mode 100644 index 945da00140..0000000000 --- a/glance/api/v1/controller.py +++ /dev/null @@ -1,96 +0,0 @@ -# Copyright 2011 OpenStack Foundation -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import glance_store as store -from oslo_log import log as logging -import webob.exc - -from glance.common import exception -from glance.i18n import _ -import glance.registry.client.v1.api as registry - - -LOG = logging.getLogger(__name__) - - -class BaseController(object): - def get_image_meta_or_404(self, request, image_id): - """ - Grabs the image metadata for an image with a supplied - identifier or raises an HTTPNotFound (404) response - - :param request: The WSGI/Webob Request object - :param image_id: The opaque image identifier - - :raises HTTPNotFound: if image does not exist - """ - context = request.context - try: - return registry.get_image_metadata(context, image_id) - except exception.NotFound: - LOG.debug("Image with identifier %s not found", image_id) - msg = _("Image with identifier %s not found") % image_id - raise webob.exc.HTTPNotFound( - msg, request=request, content_type='text/plain') - except exception.Forbidden: - LOG.debug("Forbidden image access") - raise webob.exc.HTTPForbidden(_("Forbidden image access"), - request=request, - content_type='text/plain') - - def get_active_image_meta_or_error(self, request, image_id): - """ - Same as get_image_meta_or_404 except that it will raise a 403 if the - image is deactivated or 404 if the image is otherwise not 'active'. - """ - image = self.get_image_meta_or_404(request, image_id) - if image['status'] == 'deactivated': - LOG.debug("Image %s is deactivated", image_id) - msg = _("Image %s is deactivated") % image_id - raise webob.exc.HTTPForbidden( - msg, request=request, content_type='text/plain') - if image['status'] != 'active': - LOG.debug("Image %s is not active", image_id) - msg = _("Image %s is not active") % image_id - raise webob.exc.HTTPNotFound( - msg, request=request, content_type='text/plain') - return image - - def update_store_acls(self, req, image_id, location_uri, public=False): - if location_uri: - try: - read_tenants = [] - write_tenants = [] - members = registry.get_image_members(req.context, image_id) - if members: - for member in members: - if member['can_share']: - write_tenants.append(member['member_id']) - else: - read_tenants.append(member['member_id']) - store.set_acls(location_uri, public=public, - read_tenants=read_tenants, - write_tenants=write_tenants, - context=req.context) - except store.UnknownScheme: - msg = _("Store for image_id not found: %s") % image_id - raise webob.exc.HTTPBadRequest(explanation=msg, - request=req, - content_type='text/plain') - except store.NotFound: - msg = _("Data for image_id not found: %s") % image_id - raise webob.exc.HTTPNotFound(explanation=msg, - request=req, - content_type='text/plain') diff --git a/glance/api/v1/filters.py b/glance/api/v1/filters.py deleted file mode 100644 index a71b13cc89..0000000000 --- a/glance/api/v1/filters.py +++ /dev/null @@ -1,40 +0,0 @@ -# Copyright 2012, Piston Cloud Computing, Inc. -# 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. - - -def validate(filter, value): - return FILTER_FUNCTIONS.get(filter, lambda v: True)(value) - - -def validate_int_in_range(min=0, max=None): - def _validator(v): - try: - if max is None: - return min <= int(v) - return min <= int(v) <= max - except ValueError: - return False - return _validator - - -def validate_boolean(v): - return v.lower() in ('none', 'true', 'false', '1', '0') - - -FILTER_FUNCTIONS = {'size_max': validate_int_in_range(), # build validator - 'size_min': validate_int_in_range(), # build validator - 'min_ram': validate_int_in_range(), # build validator - 'protected': validate_boolean, - 'is_public': validate_boolean, } diff --git a/glance/api/v1/router.py b/glance/api/v1/router.py index 05287ead5c..e5397b5b28 100644 --- a/glance/api/v1/router.py +++ b/glance/api/v1/router.py @@ -1,4 +1,4 @@ -# Copyright 2011 OpenStack Foundation +# Copyright 2020 Red Hat, Inc. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may @@ -13,19 +13,21 @@ # License for the specific language governing permissions and limitations # under the License. - from glance.common import wsgi -class API(wsgi.Router): +def init(mapper): + reject_resource = wsgi.Resource(wsgi.RejectMethodController()) + mapper.connect("/v1", controller=reject_resource, + action="reject") - """WSGI router for Glance v1 API requests.""" + +class API(wsgi.Router): + """WSGI entry point for satisfy grenade.""" def __init__(self, mapper): - reject_method_resource = wsgi.Resource(wsgi.RejectMethodController()) + mapper = mapper or wsgi.APIMapper() - mapper.connect("/", - controller=reject_method_resource, - action="reject") + init(mapper) super(API, self).__init__(mapper) diff --git a/glance/api/v1/upload_utils.py b/glance/api/v1/upload_utils.py deleted file mode 100644 index fb748c47fd..0000000000 --- a/glance/api/v1/upload_utils.py +++ /dev/null @@ -1,293 +0,0 @@ -# Copyright 2013 OpenStack Foundation -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -import glance_store as store_api -from oslo_config import cfg -from oslo_log import log as logging -from oslo_utils import encodeutils -from oslo_utils import excutils -import webob.exc - -from glance.common import exception -from glance.common import store_utils -from glance.common import utils -import glance.db -from glance.i18n import _, _LE, _LI -import glance.registry.client.v1.api as registry - - -CONF = cfg.CONF -LOG = logging.getLogger(__name__) - - -def initiate_deletion(req, location_data, id): - """ - Deletes image data from the location of backend store. - - :param req: The WSGI/Webob Request object - :param location_data: Location to the image data in a data store - :param id: Opaque image identifier - """ - store_utils.delete_image_location_from_backend(req.context, - id, location_data) - - -def _kill(req, image_id, from_state): - """ - Marks the image status to `killed`. - - :param req: The WSGI/Webob Request object - :param image_id: Opaque image identifier - :param from_state: Permitted current status for transition to 'killed' - """ - # TODO(dosaboy): http://docs.openstack.org/developer/glance/statuses.html - # needs updating to reflect the fact that queued->killed and saving->killed - # are both allowed. - registry.update_image_metadata(req.context, image_id, - {'status': 'killed'}, - from_state=from_state) - - -def safe_kill(req, image_id, from_state): - """ - Mark image killed without raising exceptions if it fails. - - Since _kill is meant to be called from exceptions handlers, it should - not raise itself, rather it should just log its error. - - :param req: The WSGI/Webob Request object - :param image_id: Opaque image identifier - :param from_state: Permitted current status for transition to 'killed' - """ - try: - _kill(req, image_id, from_state) - except Exception: - LOG.exception(_LE("Unable to kill image %(id)s: "), {'id': image_id}) - - -def upload_data_to_store(req, image_meta, image_data, store, notifier): - """ - Upload image data to specified store. - - Upload image data to the store and cleans up on error. - """ - image_id = image_meta['id'] - - db_api = glance.db.get_api(v1_mode=True) - image_size = image_meta.get('size') - - try: - # By default image_data will be passed as CooperativeReader object. - # But if 'user_storage_quota' is enabled and 'remaining' is not None - # then it will be passed as object of LimitingReader to - # 'store_add_to_backend' method. - image_data = utils.CooperativeReader(image_data) - - remaining = glance.api.common.check_quota( - req.context, image_size, db_api, image_id=image_id) - if remaining is not None: - image_data = utils.LimitingReader(image_data, remaining) - - (uri, - size, - checksum, - location_metadata) = store_api.store_add_to_backend( - image_meta['id'], - image_data, - image_meta['size'], - store, - context=req.context) - - location_data = {'url': uri, - 'metadata': location_metadata, - 'status': 'active'} - - try: - # recheck the quota in case there were simultaneous uploads that - # did not provide the size - glance.api.common.check_quota( - req.context, size, db_api, image_id=image_id) - except exception.StorageQuotaFull: - with excutils.save_and_reraise_exception(): - LOG.info(_LI('Cleaning up %s after exceeding ' - 'the quota'), image_id) - store_utils.safe_delete_from_backend( - req.context, image_meta['id'], location_data) - - def _kill_mismatched(image_meta, attr, actual): - supplied = image_meta.get(attr) - if supplied and supplied != actual: - msg = (_("Supplied %(attr)s (%(supplied)s) and " - "%(attr)s generated from uploaded image " - "(%(actual)s) did not match. Setting image " - "status to 'killed'.") % {'attr': attr, - 'supplied': supplied, - 'actual': actual}) - LOG.error(msg) - safe_kill(req, image_id, 'saving') - initiate_deletion(req, location_data, image_id) - raise webob.exc.HTTPBadRequest(explanation=msg, - content_type="text/plain", - request=req) - - # Verify any supplied size/checksum value matches size/checksum - # returned from store when adding image - _kill_mismatched(image_meta, 'size', size) - _kill_mismatched(image_meta, 'checksum', checksum) - - # Update the database with the checksum returned - # from the backend store - LOG.debug("Updating image %(image_id)s data. " - "Checksum set to %(checksum)s, size set " - "to %(size)d", {'image_id': image_id, - 'checksum': checksum, - 'size': size}) - update_data = {'checksum': checksum, - 'size': size} - try: - try: - state = 'saving' - image_meta = registry.update_image_metadata(req.context, - image_id, - update_data, - from_state=state) - except exception.Duplicate: - image = registry.get_image_metadata(req.context, image_id) - if image['status'] == 'deleted': - raise exception.ImageNotFound() - else: - raise - except exception.NotAuthenticated as e: - # Delete image data due to possible token expiration. - LOG.debug("Authentication error - the token may have " - "expired during file upload. Deleting image data for " - " %s", image_id) - initiate_deletion(req, location_data, image_id) - raise webob.exc.HTTPUnauthorized(explanation=e.msg, request=req) - except exception.ImageNotFound: - msg = _("Image %s could not be found after upload. The image may" - " have been deleted during the upload.") % image_id - LOG.info(msg) - - # NOTE(jculp): we need to clean up the datastore if an image - # resource is deleted while the image data is being uploaded - # - # We get "location_data" from above call to store.add(), any - # exceptions that occur there handle this same issue internally, - # Since this is store-agnostic, should apply to all stores. - initiate_deletion(req, location_data, image_id) - raise webob.exc.HTTPPreconditionFailed(explanation=msg, - request=req, - content_type='text/plain') - - except store_api.StoreAddDisabled: - msg = _("Error in store configuration. Adding images to store " - "is disabled.") - LOG.exception(msg) - safe_kill(req, image_id, 'saving') - notifier.error('image.upload', msg) - raise webob.exc.HTTPGone(explanation=msg, request=req, - content_type='text/plain') - - except (store_api.Duplicate, exception.Duplicate) as e: - msg = (_("Attempt to upload duplicate image: %s") % - encodeutils.exception_to_unicode(e)) - LOG.warn(msg) - # NOTE(dosaboy): do not delete the image since it is likely that this - # conflict is a result of another concurrent upload that will be - # successful. - notifier.error('image.upload', msg) - raise webob.exc.HTTPConflict(explanation=msg, - request=req, - content_type="text/plain") - - except exception.Forbidden as e: - msg = (_("Forbidden upload attempt: %s") % - encodeutils.exception_to_unicode(e)) - LOG.warn(msg) - safe_kill(req, image_id, 'saving') - notifier.error('image.upload', msg) - raise webob.exc.HTTPForbidden(explanation=msg, - request=req, - content_type="text/plain") - - except store_api.StorageFull as e: - msg = (_("Image storage media is full: %s") % - encodeutils.exception_to_unicode(e)) - LOG.error(msg) - safe_kill(req, image_id, 'saving') - notifier.error('image.upload', msg) - raise webob.exc.HTTPRequestEntityTooLarge(explanation=msg, - request=req, - content_type='text/plain') - - except store_api.StorageWriteDenied as e: - msg = (_("Insufficient permissions on image storage media: %s") % - encodeutils.exception_to_unicode(e)) - LOG.error(msg) - safe_kill(req, image_id, 'saving') - notifier.error('image.upload', msg) - raise webob.exc.HTTPServiceUnavailable(explanation=msg, - request=req, - content_type='text/plain') - - except exception.ImageSizeLimitExceeded: - msg = (_("Denying attempt to upload image larger than %d bytes.") - % CONF.image_size_cap) - LOG.warn(msg) - safe_kill(req, image_id, 'saving') - notifier.error('image.upload', msg) - raise webob.exc.HTTPRequestEntityTooLarge(explanation=msg, - request=req, - content_type='text/plain') - - except exception.StorageQuotaFull as e: - msg = (_("Denying attempt to upload image because it exceeds the " - "quota: %s") % encodeutils.exception_to_unicode(e)) - LOG.warn(msg) - safe_kill(req, image_id, 'saving') - notifier.error('image.upload', msg) - raise webob.exc.HTTPRequestEntityTooLarge(explanation=msg, - request=req, - content_type='text/plain') - - except webob.exc.HTTPError: - # NOTE(bcwaldon): Ideally, we would just call 'raise' here, - # but something in the above function calls is affecting the - # exception context and we must explicitly re-raise the - # caught exception. - msg = _LE("Received HTTP error while uploading image %s") % image_id - notifier.error('image.upload', msg) - with excutils.save_and_reraise_exception(): - LOG.exception(msg) - safe_kill(req, image_id, 'saving') - - except (ValueError, IOError): - msg = _("Client disconnected before sending all data to backend") - LOG.warn(msg) - safe_kill(req, image_id, 'saving') - raise webob.exc.HTTPBadRequest(explanation=msg, - content_type="text/plain", - request=req) - - except Exception: - msg = _("Failed to upload image %s") % image_id - LOG.exception(msg) - safe_kill(req, image_id, 'saving') - notifier.error('image.upload', msg) - raise webob.exc.HTTPInternalServerError(explanation=msg, - request=req, - content_type='text/plain') - - return image_meta, location_data diff --git a/glance/common/config.py b/glance/common/config.py index 2f9bf9193b..2aa08b60ed 100644 --- a/glance/common/config.py +++ b/glance/common/config.py @@ -159,16 +159,6 @@ Related Options: """)), ] -_DEPRECATE_GLANCE_V1_MSG = _('The Images (Glance) version 1 API has been ' - 'DEPRECATED in the Newton release and will be ' - 'removed on or after Pike release, following ' - 'the standard OpenStack deprecation policy. ' - 'Hence, the configuration options specific to ' - 'the Images (Glance) v1 API are hereby ' - 'deprecated and subject to removal. Operators ' - 'are advised to deploy the Images (Glance) v2 ' - 'API.') - common_opts = [ cfg.BoolOpt('allow_additional_image_properties', default=True, deprecated_for_removal=True, diff --git a/glance/common/rpc.py b/glance/common/rpc.py deleted file mode 100644 index a60cb3c121..0000000000 --- a/glance/common/rpc.py +++ /dev/null @@ -1,302 +0,0 @@ -# Copyright 2013 Red Hat, Inc. -# 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. - -""" -RPC Controller -""" -import datetime -import traceback - -from oslo_config import cfg -from oslo_log import log as logging -from oslo_utils import encodeutils -import oslo_utils.importutils as imp -import six -from webob import exc - -from glance.common import client -from glance.common import exception -from glance.common import timeutils -from glance.common import wsgi -from glance.i18n import _, _LE - -LOG = logging.getLogger(__name__) - - -rpc_opts = [ - cfg.ListOpt('allowed_rpc_exception_modules', - default=['glance.common.exception', - 'builtins', - 'exceptions', - ], - help=_(""" -List of allowed exception modules to handle RPC exceptions. - -Provide a comma separated list of modules whose exceptions are -permitted to be recreated upon receiving exception data via an RPC -call made to Glance. The default list includes -``glance.common.exception``, ``builtins``, and ``exceptions``. - -The RPC protocol permits interaction with Glance via calls across a -network or within the same system. Including a list of exception -namespaces with this option enables RPC to propagate the exceptions -back to the users. - -Possible values: - * A comma separated list of valid exception modules - -Related options: - * None -""")), -] - -CONF = cfg.CONF -CONF.register_opts(rpc_opts) - - -class RPCJSONSerializer(wsgi.JSONResponseSerializer): - - @staticmethod - def _to_primitive(_type, _value): - return {"_type": _type, "_value": _value} - - def _sanitizer(self, obj): - if isinstance(obj, datetime.datetime): - return self._to_primitive("datetime", - obj.isoformat()) - - return super(RPCJSONSerializer, self)._sanitizer(obj) - - -class RPCJSONDeserializer(wsgi.JSONRequestDeserializer): - - @staticmethod - def _to_datetime(obj): - return timeutils.normalize_time(timeutils.parse_isotime(obj)) - - def _sanitizer(self, obj): - try: - _type, _value = obj["_type"], obj["_value"] - return getattr(self, "_to_" + _type)(_value) - except (KeyError, AttributeError): - return obj - - -class Controller(object): - """ - Base RPCController. - - This is the base controller for RPC based APIs. Commands - handled by this controller respect the following form: - - :: - - [{ - 'command': 'method_name', - 'kwargs': {...} - }] - - The controller is capable of processing more than one command - per request and will always return a list of results. - - :param bool raise_exc: Specifies whether to raise - exceptions instead of "serializing" them. - - """ - - def __init__(self, raise_exc=False): - self._registered = {} - self.raise_exc = raise_exc - - def register(self, resource, filtered=None, excluded=None, refiner=None): - """ - Exports methods through the RPC Api. - - :param resource: Resource's instance to register. - :param filtered: List of methods that *can* be registered. Read - as "Method must be in this list". - :param excluded: List of methods to exclude. - :param refiner: Callable to use as filter for methods. - - :raises TypeError: If refiner is not callable. - - """ - - funcs = [x for x in dir(resource) if not x.startswith("_")] - - if filtered: - funcs = [f for f in funcs if f in filtered] - - if excluded: - funcs = [f for f in funcs if f not in excluded] - - if refiner: - funcs = filter(refiner, funcs) - - for name in funcs: - meth = getattr(resource, name) - - if not callable(meth): - continue - - self._registered[name] = meth - - def __call__(self, req, body): - """ - Executes the command - """ - - if not isinstance(body, list): - msg = _("Request must be a list of commands") - raise exc.HTTPBadRequest(explanation=msg) - - def validate(cmd): - if not isinstance(cmd, dict): - msg = _("Bad Command: %s") % str(cmd) - raise exc.HTTPBadRequest(explanation=msg) - - command, kwargs = cmd.get("command"), cmd.get("kwargs") - - if (not command or not isinstance(command, six.string_types) or - (kwargs and not isinstance(kwargs, dict))): - msg = _("Wrong command structure: %s") % (str(cmd)) - raise exc.HTTPBadRequest(explanation=msg) - - method = self._registered.get(command) - if not method: - # Just raise 404 if the user tries to - # access a private method. No need for - # 403 here since logically the command - # is not registered to the rpc dispatcher - raise exc.HTTPNotFound(explanation=_("Command not found")) - - return True - - # If more than one command were sent then they might - # be intended to be executed sequentially, that for, - # lets first verify they're all valid before executing - # them. - commands = filter(validate, body) - - results = [] - for cmd in commands: - # kwargs is not required - command, kwargs = cmd["command"], cmd.get("kwargs", {}) - method = self._registered[command] - try: - result = method(req.context, **kwargs) - except Exception as e: - if self.raise_exc: - raise - - cls, val = e.__class__, encodeutils.exception_to_unicode(e) - msg = (_LE("RPC Call Error: %(val)s\n%(tb)s") % - dict(val=val, tb=traceback.format_exc())) - LOG.error(msg) - - # NOTE(flaper87): Don't propagate all exceptions - # but the ones allowed by the user. - module = cls.__module__ - if module not in CONF.allowed_rpc_exception_modules: - cls = exception.RPCError - val = encodeutils.exception_to_unicode( - exception.RPCError(cls=cls, val=val)) - - cls_path = "%s.%s" % (cls.__module__, cls.__name__) - result = {"_error": {"cls": cls_path, "val": val}} - results.append(result) - return results - - -class RPCClient(client.BaseClient): - - def __init__(self, *args, **kwargs): - self._serializer = RPCJSONSerializer() - self._deserializer = RPCJSONDeserializer() - - self.raise_exc = kwargs.pop("raise_exc", True) - self.base_path = kwargs.pop("base_path", '/rpc') - super(RPCClient, self).__init__(*args, **kwargs) - - @client.handle_unauthenticated - def bulk_request(self, commands): - """ - Execute multiple commands in a single request. - - :param commands: List of commands to send. Commands - must respect the following form - - :: - - { - 'command': 'method_name', - 'kwargs': method_kwargs - } - - """ - body = self._serializer.to_json(commands) - response = super(RPCClient, self).do_request('POST', - self.base_path, - body) - return self._deserializer.from_json(response.read()) - - def do_request(self, method, **kwargs): - """ - Simple do_request override. This method serializes - the outgoing body and builds the command that will - be sent. - - :param method: The remote python method to call - :param kwargs: Dynamic parameters that will be - passed to the remote method. - """ - content = self.bulk_request([{'command': method, - 'kwargs': kwargs}]) - - # NOTE(flaper87): Return the first result if - # a single command was executed. - content = content[0] - - # NOTE(flaper87): Check if content is an error - # and re-raise it if raise_exc is True. Before - # checking if content contains the '_error' key, - # verify if it is an instance of dict - since the - # RPC call may have returned something different. - if self.raise_exc and (isinstance(content, dict) - and '_error' in content): - error = content['_error'] - try: - exc_cls = imp.import_class(error['cls']) - raise exc_cls(error['val']) - except ImportError: - # NOTE(flaper87): The exception - # class couldn't be imported, using - # a generic exception. - raise exception.RPCError(**error) - return content - - def __getattr__(self, item): - """ - This method returns a method_proxy that - will execute the rpc call in the registry - service. - """ - if item.startswith('_'): - raise AttributeError(item) - - def method_proxy(**kw): - return self.do_request(item, **kw) - - return method_proxy diff --git a/glance/db/registry/__init__.py b/glance/db/registry/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/glance/db/registry/api.py b/glance/db/registry/api.py deleted file mode 100644 index f4f7aef28c..0000000000 --- a/glance/db/registry/api.py +++ /dev/null @@ -1,546 +0,0 @@ -# Copyright 2013 Red Hat, Inc. -# Copyright 2015 Mirantis, Inc. -# 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. - -""" -This is the Registry's Driver API. - -This API relies on the registry RPC client (version >= 2). The functions bellow -work as a proxy for the database back-end configured in the registry service, -which means that everything returned by that back-end will be also returned by -this API. - - -This API exists for supporting deployments not willing to put database -credentials in glance-api. Those deployments can rely on this registry driver -that will talk to a remote registry service, which will then access the -database back-end. -""" - -import functools - -from glance.db import utils as db_utils -from glance.registry.client.v2 import api - - -def configure(): - api.configure_registry_client() - - -def _get_client(func): - """Injects a client instance to the each function - - This decorator creates an instance of the Registry - client and passes it as an argument to each function - in this API. - """ - @functools.wraps(func) - def wrapper(context, *args, **kwargs): - client = api.get_registry_client(context) - return func(client, *args, **kwargs) - return wrapper - - -@_get_client -def image_create(client, values, v1_mode=False): - """Create an image from the values dictionary.""" - return client.image_create(values=values, v1_mode=v1_mode) - - -@_get_client -def image_update(client, image_id, values, purge_props=False, from_state=None, - v1_mode=False): - """ - Set the given properties on an image and update it. - - :raises ImageNotFound: if image does not exist. - """ - return client.image_update(values=values, - image_id=image_id, - purge_props=purge_props, - from_state=from_state, - v1_mode=v1_mode) - - -@_get_client -def image_destroy(client, image_id): - """Destroy the image or raise if it does not exist.""" - return client.image_destroy(image_id=image_id) - - -@_get_client -def image_get(client, image_id, force_show_deleted=False, v1_mode=False): - return client.image_get(image_id=image_id, - force_show_deleted=force_show_deleted, - v1_mode=v1_mode) - - -def is_image_visible(context, image, status=None): - """Return True if the image is visible in this context.""" - return db_utils.is_image_visible(context, image, image_member_find, status) - - -@_get_client -def image_get_all(client, filters=None, marker=None, limit=None, - sort_key=None, sort_dir=None, - member_status='accepted', is_public=None, - admin_as_user=False, return_tag=False, v1_mode=False): - """ - Get all images that match zero or more filters. - - :param filters: dict of filter keys and values. If a 'properties' - key is present, it is treated as a dict of key/value - filters on the image properties attribute - :param marker: image id after which to start page - :param limit: maximum number of images to return - :param sort_key: image attribute by which results should be sorted - :param sort_dir: direction in which results should be sorted (asc, desc) - :param member_status: only return shared images that have this membership - status - :param is_public: If true, return only public images. If false, return - only private and shared images. - :param admin_as_user: For backwards compatibility. If true, then return to - an admin the equivalent set of images which it would see - if it were a regular user - :param return_tag: To indicates whether image entry in result includes it - relevant tag entries. This could improve upper-layer - query performance, to prevent using separated calls - :param v1_mode: If true, mutates the 'visibility' value of each image - into the v1-compatible field 'is_public' - """ - sort_key = ['created_at'] if not sort_key else sort_key - sort_dir = ['desc'] if not sort_dir else sort_dir - return client.image_get_all(filters=filters, marker=marker, limit=limit, - sort_key=sort_key, sort_dir=sort_dir, - member_status=member_status, - is_public=is_public, - admin_as_user=admin_as_user, - return_tag=return_tag, - v1_mode=v1_mode) - - -@_get_client -def image_property_create(client, values, session=None): - """Create an ImageProperty object""" - return client.image_property_create(values=values) - - -@_get_client -def image_property_delete(client, prop_ref, image_ref, session=None): - """ - Used internally by _image_property_create and image_property_update - """ - return client.image_property_delete(prop_ref=prop_ref, image_ref=image_ref) - - -@_get_client -def image_member_create(client, values, session=None): - """Create an ImageMember object""" - return client.image_member_create(values=values) - - -@_get_client -def image_member_update(client, memb_id, values): - """Update an ImageMember object""" - return client.image_member_update(memb_id=memb_id, values=values) - - -@_get_client -def image_member_delete(client, memb_id, session=None): - """Delete an ImageMember object""" - client.image_member_delete(memb_id=memb_id) - - -@_get_client -def image_member_find(client, image_id=None, member=None, status=None, - include_deleted=False): - """Find all members that meet the given criteria. - - Note, currently include_deleted should be true only when create a new - image membership, as there may be a deleted image membership between - the same image and tenant, the membership will be reused in this case. - It should be false in other cases. - - :param image_id: identifier of image entity - :param member: tenant to which membership has been granted - :include_deleted: A boolean indicating whether the result should include - the deleted record of image member - """ - return client.image_member_find(image_id=image_id, - member=member, - status=status, - include_deleted=include_deleted) - - -@_get_client -def image_member_count(client, image_id): - """Return the number of image members for this image - - :param image_id: identifier of image entity - """ - return client.image_member_count(image_id=image_id) - - -@_get_client -def image_tag_set_all(client, image_id, tags): - client.image_tag_set_all(image_id=image_id, tags=tags) - - -@_get_client -def image_tag_create(client, image_id, value, session=None): - """Create an image tag.""" - return client.image_tag_create(image_id=image_id, value=value) - - -@_get_client -def image_tag_delete(client, image_id, value, session=None): - """Delete an image tag.""" - client.image_tag_delete(image_id=image_id, value=value) - - -@_get_client -def image_tag_get_all(client, image_id, session=None): - """Get a list of tags for a specific image.""" - return client.image_tag_get_all(image_id=image_id) - - -@_get_client -def image_location_delete(client, image_id, location_id, status, session=None): - """Delete an image location.""" - client.image_location_delete(image_id=image_id, location_id=location_id, - status=status) - - -@_get_client -def image_location_update(client, image_id, location, session=None): - """Update image location.""" - client.image_location_update(image_id=image_id, location=location) - - -@_get_client -def user_get_storage_usage(client, owner_id, image_id=None, session=None): - return client.user_get_storage_usage(owner_id=owner_id, image_id=image_id) - - -@_get_client -def task_get(client, task_id, session=None, force_show_deleted=False): - """Get a single task object - :returns: task dictionary - """ - return client.task_get(task_id=task_id, session=session, - force_show_deleted=force_show_deleted) - - -@_get_client -def task_get_all(client, filters=None, marker=None, limit=None, - sort_key='created_at', sort_dir='desc', admin_as_user=False): - """Get all tasks that match zero or more filters. - - :param filters: dict of filter keys and values. - :param marker: task id after which to start page - :param limit: maximum number of tasks to return - :param sort_key: task attribute by which results should be sorted - :param sort_dir: direction in which results should be sorted (asc, desc) - :param admin_as_user: For backwards compatibility. If true, then return to - an admin the equivalent set of tasks which it would see - if it were a regular user - :returns: tasks set - """ - return client.task_get_all(filters=filters, marker=marker, limit=limit, - sort_key=sort_key, sort_dir=sort_dir, - admin_as_user=admin_as_user) - - -@_get_client -def task_create(client, values, session=None): - """Create a task object""" - return client.task_create(values=values, session=session) - - -@_get_client -def task_delete(client, task_id, session=None): - """Delete a task object""" - return client.task_delete(task_id=task_id, session=session) - - -@_get_client -def task_update(client, task_id, values, session=None): - return client.task_update(task_id=task_id, values=values, session=session) - - -# Metadef -@_get_client -def metadef_namespace_get_all( - client, marker=None, limit=None, sort_key='created_at', - sort_dir=None, filters=None, session=None): - return client.metadef_namespace_get_all( - marker=marker, limit=limit, - sort_key=sort_key, sort_dir=sort_dir, filters=filters) - - -@_get_client -def metadef_namespace_get(client, namespace_name, session=None): - return client.metadef_namespace_get(namespace_name=namespace_name) - - -@_get_client -def metadef_namespace_create(client, values, session=None): - return client.metadef_namespace_create(values=values) - - -@_get_client -def metadef_namespace_update( - client, namespace_id, namespace_dict, - session=None): - return client.metadef_namespace_update( - namespace_id=namespace_id, namespace_dict=namespace_dict) - - -@_get_client -def metadef_namespace_delete(client, namespace_name, session=None): - return client.metadef_namespace_delete( - namespace_name=namespace_name) - - -@_get_client -def metadef_object_get_all(client, namespace_name, session=None): - return client.metadef_object_get_all( - namespace_name=namespace_name) - - -@_get_client -def metadef_object_get( - client, - namespace_name, object_name, session=None): - return client.metadef_object_get( - namespace_name=namespace_name, object_name=object_name) - - -@_get_client -def metadef_object_create( - client, - namespace_name, object_dict, session=None): - return client.metadef_object_create( - namespace_name=namespace_name, object_dict=object_dict) - - -@_get_client -def metadef_object_update( - client, - namespace_name, object_id, - object_dict, session=None): - return client.metadef_object_update( - namespace_name=namespace_name, object_id=object_id, - object_dict=object_dict) - - -@_get_client -def metadef_object_delete( - client, - namespace_name, object_name, - session=None): - return client.metadef_object_delete( - namespace_name=namespace_name, object_name=object_name) - - -@_get_client -def metadef_object_delete_namespace_content( - client, - namespace_name, session=None): - return client.metadef_object_delete_namespace_content( - namespace_name=namespace_name) - - -@_get_client -def metadef_object_count( - client, - namespace_name, session=None): - return client.metadef_object_count( - namespace_name=namespace_name) - - -@_get_client -def metadef_property_get_all( - client, - namespace_name, session=None): - return client.metadef_property_get_all( - namespace_name=namespace_name) - - -@_get_client -def metadef_property_get( - client, - namespace_name, property_name, - session=None): - return client.metadef_property_get( - namespace_name=namespace_name, property_name=property_name) - - -@_get_client -def metadef_property_create( - client, - namespace_name, property_dict, - session=None): - return client.metadef_property_create( - namespace_name=namespace_name, property_dict=property_dict) - - -@_get_client -def metadef_property_update( - client, - namespace_name, property_id, - property_dict, session=None): - return client.metadef_property_update( - namespace_name=namespace_name, property_id=property_id, - property_dict=property_dict) - - -@_get_client -def metadef_property_delete( - client, - namespace_name, property_name, - session=None): - return client.metadef_property_delete( - namespace_name=namespace_name, property_name=property_name) - - -@_get_client -def metadef_property_delete_namespace_content( - client, - namespace_name, session=None): - return client.metadef_property_delete_namespace_content( - namespace_name=namespace_name) - - -@_get_client -def metadef_property_count( - client, - namespace_name, session=None): - return client.metadef_property_count( - namespace_name=namespace_name) - - -@_get_client -def metadef_resource_type_create(client, values, session=None): - return client.metadef_resource_type_create(values=values) - - -@_get_client -def metadef_resource_type_get( - client, - resource_type_name, session=None): - return client.metadef_resource_type_get( - resource_type_name=resource_type_name) - - -@_get_client -def metadef_resource_type_get_all(client, session=None): - return client.metadef_resource_type_get_all() - - -@_get_client -def metadef_resource_type_delete( - client, - resource_type_name, session=None): - return client.metadef_resource_type_delete( - resource_type_name=resource_type_name) - - -@_get_client -def metadef_resource_type_association_get( - client, - namespace_name, resource_type_name, - session=None): - return client.metadef_resource_type_association_get( - namespace_name=namespace_name, resource_type_name=resource_type_name) - - -@_get_client -def metadef_resource_type_association_create( - client, - namespace_name, values, session=None): - return client.metadef_resource_type_association_create( - namespace_name=namespace_name, values=values) - - -@_get_client -def metadef_resource_type_association_delete( - client, - namespace_name, resource_type_name, session=None): - return client.metadef_resource_type_association_delete( - namespace_name=namespace_name, resource_type_name=resource_type_name) - - -@_get_client -def metadef_resource_type_association_get_all_by_namespace( - client, - namespace_name, session=None): - return client.metadef_resource_type_association_get_all_by_namespace( - namespace_name=namespace_name) - - -@_get_client -def metadef_tag_get_all(client, namespace_name, filters=None, marker=None, - limit=None, sort_key='created_at', sort_dir=None, - session=None): - return client.metadef_tag_get_all( - namespace_name=namespace_name, filters=filters, marker=marker, - limit=limit, sort_key=sort_key, sort_dir=sort_dir, session=session) - - -@_get_client -def metadef_tag_get(client, namespace_name, name, session=None): - return client.metadef_tag_get( - namespace_name=namespace_name, name=name) - - -@_get_client -def metadef_tag_create( - client, namespace_name, tag_dict, session=None): - return client.metadef_tag_create( - namespace_name=namespace_name, tag_dict=tag_dict) - - -@_get_client -def metadef_tag_create_tags( - client, namespace_name, tag_list, session=None): - return client.metadef_tag_create_tags( - namespace_name=namespace_name, tag_list=tag_list) - - -@_get_client -def metadef_tag_update( - client, namespace_name, id, tag_dict, session=None): - return client.metadef_tag_update( - namespace_name=namespace_name, id=id, tag_dict=tag_dict) - - -@_get_client -def metadef_tag_delete( - client, namespace_name, name, session=None): - return client.metadef_tag_delete( - namespace_name=namespace_name, name=name) - - -@_get_client -def metadef_tag_delete_namespace_content( - client, namespace_name, session=None): - return client.metadef_tag_delete_namespace_content( - namespace_name=namespace_name) - - -@_get_client -def metadef_tag_count(client, namespace_name, session=None): - return client.metadef_tag_count(namespace_name=namespace_name) diff --git a/glance/hacking/checks.py b/glance/hacking/checks.py index 0ead27a1f1..a902b06b32 100644 --- a/glance/hacking/checks.py +++ b/glance/hacking/checks.py @@ -89,7 +89,6 @@ def no_translate_debug_logs(logical_line, filename): "glance/domain", "glance/image_cache", "glance/quota", - "glance/registry", "glance/store", "glance/tests", ] diff --git a/glance/opts.py b/glance/opts.py index b0067d304c..7f14f057d3 100644 --- a/glance/opts.py +++ b/glance/opts.py @@ -36,7 +36,6 @@ import glance.common.config import glance.common.location_strategy import glance.common.location_strategy.store_type import glance.common.property_utils -import glance.common.rpc import glance.common.wsgi import glance.image_cache import glance.image_cache.drivers.sqlite @@ -51,7 +50,6 @@ _api_opts = [ glance.common.config.common_opts, glance.common.location_strategy.location_strategy_opts, glance.common.property_utils.property_opts, - glance.common.rpc.rpc_opts, glance.common.wsgi.bind_opts, glance.common.wsgi.eventlet_opts, glance.common.wsgi.socket_opts, diff --git a/glance/registry/__init__.py b/glance/registry/__init__.py deleted file mode 100644 index e7bcfbcee4..0000000000 --- a/glance/registry/__init__.py +++ /dev/null @@ -1,68 +0,0 @@ -# Copyright 2010-2011 OpenStack Foundation -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -""" -Registry API -""" - -from oslo_config import cfg - -from glance.i18n import _ - - -registry_addr_opts = [ - cfg.HostAddressOpt('registry_host', - default='0.0.0.0', - deprecated_for_removal=True, - deprecated_since="Queens", - deprecated_reason=_(""" -Glance registry service is deprecated for removal. - -More information can be found from the spec: -http://specs.openstack.org/openstack/glance-specs/specs/queens/approved/glance/deprecate-registry.html -"""), - help=_(""" -Address the registry server is hosted on. - -Possible values: - * A valid IP or hostname - -Related options: - * None - -""")), - cfg.PortOpt('registry_port', default=9191, - deprecated_for_removal=True, - deprecated_since="Queens", - deprecated_reason=_(""" -Glance registry service is deprecated for removal. - -More information can be found from the spec: -http://specs.openstack.org/openstack/glance-specs/specs/queens/approved/glance/deprecate-registry.html -"""), - help=_(""" -Port the registry server is listening on. - -Possible values: - * A valid port number - -Related options: - * None - -""")), -] - -CONF = cfg.CONF -CONF.register_opts(registry_addr_opts) diff --git a/glance/registry/api/__init__.py b/glance/registry/api/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/glance/registry/api/v1/__init__.py b/glance/registry/api/v1/__init__.py deleted file mode 100644 index 0e2b41c4ca..0000000000 --- a/glance/registry/api/v1/__init__.py +++ /dev/null @@ -1,91 +0,0 @@ -# Copyright 2010-2011 OpenStack Foundation -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from glance.common import wsgi -from glance.registry.api.v1 import images -from glance.registry.api.v1 import members - - -def init(mapper): - images_resource = images.create_resource() - - mapper.connect("/", - controller=images_resource, - action="index") - mapper.connect("/images", - controller=images_resource, - action="index", - conditions={'method': ['GET']}) - mapper.connect("/images", - controller=images_resource, - action="create", - conditions={'method': ['POST']}) - mapper.connect("/images/detail", - controller=images_resource, - action="detail", - conditions={'method': ['GET']}) - mapper.connect("/images/{id}", - controller=images_resource, - action="show", - conditions=dict(method=["GET"])) - mapper.connect("/images/{id}", - controller=images_resource, - action="update", - conditions=dict(method=["PUT"])) - mapper.connect("/images/{id}", - controller=images_resource, - action="delete", - conditions=dict(method=["DELETE"])) - - members_resource = members.create_resource() - - mapper.connect("/images/{image_id}/members", - controller=members_resource, - action="index", - conditions={'method': ['GET']}) - mapper.connect("/images/{image_id}/members", - controller=members_resource, - action="create", - conditions={'method': ['POST']}) - mapper.connect("/images/{image_id}/members", - controller=members_resource, - action="update_all", - conditions=dict(method=["PUT"])) - mapper.connect("/images/{image_id}/members/{id}", - controller=members_resource, - action="show", - conditions={'method': ['GET']}) - mapper.connect("/images/{image_id}/members/{id}", - controller=members_resource, - action="update", - conditions={'method': ['PUT']}) - mapper.connect("/images/{image_id}/members/{id}", - controller=members_resource, - action="delete", - conditions={'method': ['DELETE']}) - mapper.connect("/shared-images/{id}", - controller=members_resource, - action="index_shared_images") - - -class API(wsgi.Router): - """WSGI entry point for all Registry requests.""" - - def __init__(self, mapper): - mapper = mapper or wsgi.APIMapper() - - init(mapper) - - super(API, self).__init__(mapper) diff --git a/glance/registry/api/v1/images.py b/glance/registry/api/v1/images.py deleted file mode 100644 index 322a70035a..0000000000 --- a/glance/registry/api/v1/images.py +++ /dev/null @@ -1,569 +0,0 @@ -# Copyright 2010-2011 OpenStack Foundation -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -""" -Reference implementation registry server WSGI controller -""" - -from oslo_config import cfg -from oslo_log import log as logging -from oslo_utils import encodeutils -from oslo_utils import strutils -from oslo_utils import uuidutils -from webob import exc - -from glance.common import exception -from glance.common import timeutils -from glance.common import utils -from glance.common import wsgi -import glance.db -from glance.i18n import _, _LE, _LI, _LW - - -LOG = logging.getLogger(__name__) - -CONF = cfg.CONF - -DISPLAY_FIELDS_IN_INDEX = ['id', 'name', 'size', - 'disk_format', 'container_format', - 'checksum'] - -SUPPORTED_FILTERS = ['name', 'status', 'container_format', 'disk_format', - 'min_ram', 'min_disk', 'size_min', 'size_max', - 'changes-since', 'protected'] - -SUPPORTED_SORT_KEYS = ('name', 'status', 'container_format', 'disk_format', - 'size', 'id', 'created_at', 'updated_at') - -SUPPORTED_SORT_DIRS = ('asc', 'desc') - -SUPPORTED_PARAMS = ('limit', 'marker', 'sort_key', 'sort_dir') - - -def _normalize_image_location_for_db(image_data): - """ - This function takes the legacy locations field and the newly added - location_data field from the image_data values dictionary which flows - over the wire between the registry and API servers and converts it - into the location_data format only which is then consumable by the - Image object. - - :param image_data: a dict of values representing information in the image - :returns: a new image data dict - """ - if 'locations' not in image_data and 'location_data' not in image_data: - image_data['locations'] = None - return image_data - - locations = image_data.pop('locations', []) - location_data = image_data.pop('location_data', []) - - location_data_dict = {} - for l in locations: - location_data_dict[l] = {} - for l in location_data: - location_data_dict[l['url']] = {'metadata': l['metadata'], - 'status': l['status'], - # Note(zhiyan): New location has no ID. - 'id': l['id'] if 'id' in l else None} - - # NOTE(jbresnah) preserve original order. tests assume original order, - # should that be defined functionality - ordered_keys = locations[:] - for ld in location_data: - if ld['url'] not in ordered_keys: - ordered_keys.append(ld['url']) - - location_data = [] - for loc in ordered_keys: - data = location_data_dict[loc] - if data: - location_data.append({'url': loc, - 'metadata': data['metadata'], - 'status': data['status'], - 'id': data['id']}) - else: - location_data.append({'url': loc, - 'metadata': {}, - 'status': 'active', - 'id': None}) - - image_data['locations'] = location_data - return image_data - - -class Controller(object): - - def __init__(self): - self.db_api = glance.db.get_api() - - def _get_images(self, context, filters, **params): - """Get images, wrapping in exception if necessary.""" - # NOTE(markwash): for backwards compatibility, is_public=True for - # admins actually means "treat me as if I'm not an admin and show me - # all my images" - if context.is_admin and params.get('is_public') is True: - params['admin_as_user'] = True - del params['is_public'] - try: - return self.db_api.image_get_all(context, filters=filters, - v1_mode=True, **params) - except exception.ImageNotFound: - LOG.warn(_LW("Invalid marker. Image %(id)s could not be " - "found."), {'id': params.get('marker')}) - msg = _("Invalid marker. Image could not be found.") - raise exc.HTTPBadRequest(explanation=msg) - except exception.Forbidden: - LOG.warn(_LW("Access denied to image %(id)s but returning " - "'not found'"), {'id': params.get('marker')}) - msg = _("Invalid marker. Image could not be found.") - raise exc.HTTPBadRequest(explanation=msg) - except Exception: - LOG.exception(_LE("Unable to get images")) - raise - - def index(self, req): - """Return a basic filtered list of public, non-deleted images - - :param req: the Request object coming from the wsgi layer - :returns: a mapping of the following form - - .. code-block:: python - - dict(images=[image_list]) - - Where image_list is a sequence of mappings - - :: - - { - 'id': , - 'name': , - 'size': , - 'disk_format': , - 'container_format': , - 'checksum': - } - - """ - params = self._get_query_params(req) - images = self._get_images(req.context, **params) - - results = [] - for image in images: - result = {} - for field in DISPLAY_FIELDS_IN_INDEX: - result[field] = image[field] - results.append(result) - - LOG.debug("Returning image list") - return dict(images=results) - - def detail(self, req): - """Return a filtered list of public, non-deleted images in detail - - :param req: the Request object coming from the wsgi layer - :returns: a mapping of the following form - - :: - - {'images': - [{ - 'id': , - 'name': , - 'size': , - 'disk_format': , - 'container_format': , - 'checksum': , - 'min_disk': , - 'min_ram': , - 'store': , - 'status': , - 'created_at': , - 'updated_at': , - 'deleted_at': |, - 'properties': {'distro': 'Ubuntu 10.04 LTS', {...}} - }, {...}] - } - - """ - params = self._get_query_params(req) - - images = self._get_images(req.context, **params) - image_dicts = [make_image_dict(i) for i in images] - LOG.debug("Returning detailed image list") - return dict(images=image_dicts) - - def _get_query_params(self, req): - """Extract necessary query parameters from http request. - - :param req: the Request object coming from the wsgi layer - :returns: dictionary of filters to apply to list of images - """ - params = { - 'filters': self._get_filters(req), - 'limit': self._get_limit(req), - 'sort_key': [self._get_sort_key(req)], - 'sort_dir': [self._get_sort_dir(req)], - 'marker': self._get_marker(req), - } - - if req.context.is_admin: - # Only admin gets to look for non-public images - params['is_public'] = self._get_is_public(req) - - # need to coy items because the params is modified in the loop body - items = list(params.items()) - for key, value in items: - if value is None: - del params[key] - - # Fix for LP Bug #1132294 - # Ensure all shared images are returned in v1 - params['member_status'] = 'all' - return params - - def _get_filters(self, req): - """Return a dictionary of query param filters from the request - - :param req: the Request object coming from the wsgi layer - :returns: a dict of key/value filters - """ - filters = {} - properties = {} - - for param in req.params: - if param in SUPPORTED_FILTERS: - filters[param] = req.params.get(param) - if param.startswith('property-'): - _param = param[9:] - properties[_param] = req.params.get(param) - - if 'changes-since' in filters: - isotime = filters['changes-since'] - try: - filters['changes-since'] = timeutils.parse_isotime(isotime) - except ValueError: - raise exc.HTTPBadRequest(_("Unrecognized changes-since value")) - - if 'protected' in filters: - value = self._get_bool(filters['protected']) - if value is None: - raise exc.HTTPBadRequest(_("protected must be True, or " - "False")) - - filters['protected'] = value - - # only allow admins to filter on 'deleted' - if req.context.is_admin: - deleted_filter = self._parse_deleted_filter(req) - if deleted_filter is not None: - filters['deleted'] = deleted_filter - elif 'changes-since' not in filters: - filters['deleted'] = False - elif 'changes-since' not in filters: - filters['deleted'] = False - - if properties: - filters['properties'] = properties - - return filters - - def _get_limit(self, req): - """Parse a limit query param into something usable.""" - try: - limit = int(req.params.get('limit', CONF.limit_param_default)) - except ValueError: - raise exc.HTTPBadRequest(_("limit param must be an integer")) - - if limit < 0: - raise exc.HTTPBadRequest(_("limit param must be positive")) - - return min(CONF.api_limit_max, limit) - - def _get_marker(self, req): - """Parse a marker query param into something usable.""" - marker = req.params.get('marker') - - if marker and not uuidutils.is_uuid_like(marker): - msg = _('Invalid marker format') - raise exc.HTTPBadRequest(explanation=msg) - - return marker - - def _get_sort_key(self, req): - """Parse a sort key query param from the request object.""" - sort_key = req.params.get('sort_key', 'created_at') - if sort_key is not None and sort_key not in SUPPORTED_SORT_KEYS: - _keys = ', '.join(SUPPORTED_SORT_KEYS) - msg = _("Unsupported sort_key. Acceptable values: %s") % (_keys,) - raise exc.HTTPBadRequest(explanation=msg) - return sort_key - - def _get_sort_dir(self, req): - """Parse a sort direction query param from the request object.""" - sort_dir = req.params.get('sort_dir', 'desc') - if sort_dir is not None and sort_dir not in SUPPORTED_SORT_DIRS: - _keys = ', '.join(SUPPORTED_SORT_DIRS) - msg = _("Unsupported sort_dir. Acceptable values: %s") % (_keys,) - raise exc.HTTPBadRequest(explanation=msg) - return sort_dir - - def _get_bool(self, value): - value = value.lower() - if value == 'true' or value == '1': - return True - elif value == 'false' or value == '0': - return False - - return None - - def _get_is_public(self, req): - """Parse is_public into something usable.""" - is_public = req.params.get('is_public') - - if is_public is None: - # NOTE(vish): This preserves the default value of showing only - # public images. - return True - elif is_public.lower() == 'none': - return None - - value = self._get_bool(is_public) - if value is None: - raise exc.HTTPBadRequest(_("is_public must be None, True, or " - "False")) - - return value - - def _parse_deleted_filter(self, req): - """Parse deleted into something usable.""" - deleted = req.params.get('deleted') - if deleted is None: - return None - return strutils.bool_from_string(deleted) - - def show(self, req, id): - """Return data about the given image id.""" - try: - image = self.db_api.image_get(req.context, id, v1_mode=True) - LOG.debug("Successfully retrieved image %(id)s", {'id': id}) - except exception.ImageNotFound: - LOG.info(_LI("Image %(id)s not found"), {'id': id}) - raise exc.HTTPNotFound() - except exception.Forbidden: - # If it's private and doesn't belong to them, don't let on - # that it exists - LOG.info(_LI("Access denied to image %(id)s but returning" - " 'not found'"), {'id': id}) - raise exc.HTTPNotFound() - except Exception: - LOG.exception(_LE("Unable to show image %s"), id) - raise - - return dict(image=make_image_dict(image)) - - @utils.mutating - def delete(self, req, id): - """Deletes an existing image with the registry. - - :param req: wsgi Request object - :param id: The opaque internal identifier for the image - - :returns: 200 if delete was successful, a fault if not. On - success, the body contains the deleted image - information as a mapping. - """ - try: - deleted_image = self.db_api.image_destroy(req.context, id) - LOG.info(_LI("Successfully deleted image %(id)s"), {'id': id}) - return dict(image=make_image_dict(deleted_image)) - except exception.ForbiddenPublicImage: - LOG.info(_LI("Delete denied for public image %(id)s"), {'id': id}) - raise exc.HTTPForbidden() - except exception.Forbidden: - # If it's private and doesn't belong to them, don't let on - # that it exists - LOG.info(_LI("Access denied to image %(id)s but returning" - " 'not found'"), {'id': id}) - return exc.HTTPNotFound() - except exception.ImageNotFound: - LOG.info(_LI("Image %(id)s not found"), {'id': id}) - return exc.HTTPNotFound() - except Exception: - LOG.exception(_LE("Unable to delete image %s"), id) - raise - - @utils.mutating - def create(self, req, body): - """Registers a new image with the registry. - - :param req: wsgi Request object - :param body: Dictionary of information about the image - - :returns: The newly-created image information as a mapping, - which will include the newly-created image's internal id - in the 'id' field - """ - image_data = body['image'] - - # Ensure the image has a status set - image_data.setdefault('status', 'active') - - # Set up the image owner - if not req.context.is_admin or 'owner' not in image_data: - image_data['owner'] = req.context.owner - - image_id = image_data.get('id') - if image_id and not uuidutils.is_uuid_like(image_id): - LOG.info(_LI("Rejecting image creation request for invalid image " - "id '%(bad_id)s'"), {'bad_id': image_id}) - msg = _("Invalid image id format") - return exc.HTTPBadRequest(explanation=msg) - - if 'location' in image_data: - image_data['locations'] = [image_data.pop('location')] - - try: - image_data = _normalize_image_location_for_db(image_data) - image_data = self.db_api.image_create(req.context, image_data, - v1_mode=True) - image_data = dict(image=make_image_dict(image_data)) - LOG.info(_LI("Successfully created image %(id)s"), - {'id': image_data['image']['id']}) - return image_data - except exception.Duplicate: - msg = _("Image with identifier %s already exists!") % image_id - LOG.warn(msg) - return exc.HTTPConflict(msg) - except exception.Invalid as e: - msg = (_("Failed to add image metadata. " - "Got error: %s") % encodeutils.exception_to_unicode(e)) - LOG.error(msg) - return exc.HTTPBadRequest(msg) - except Exception: - LOG.exception(_LE("Unable to create image %s"), image_id) - raise - - @utils.mutating - def update(self, req, id, body): - """Updates an existing image with the registry. - - :param req: wsgi Request object - :param body: Dictionary of information about the image - :param id: The opaque internal identifier for the image - - :returns: Returns the updated image information as a mapping, - """ - image_data = body['image'] - from_state = body.get('from_state') - - # Prohibit modification of 'owner' - if not req.context.is_admin and 'owner' in image_data: - del image_data['owner'] - - if 'location' in image_data: - image_data['locations'] = [image_data.pop('location')] - - purge_props = req.headers.get("X-Glance-Registry-Purge-Props", "false") - try: - # These fields hold sensitive data, which should not be printed in - # the logs. - sensitive_fields = ['locations', 'location_data'] - LOG.debug("Updating image %(id)s with metadata: %(image_data)r", - {'id': id, - 'image_data': {k: v for k, v in image_data.items() - if k not in sensitive_fields}}) - image_data = _normalize_image_location_for_db(image_data) - if purge_props == "true": - purge_props = True - else: - purge_props = False - - updated_image = self.db_api.image_update(req.context, id, - image_data, - purge_props=purge_props, - from_state=from_state, - v1_mode=True) - - LOG.info(_LI("Updating metadata for image %(id)s"), {'id': id}) - return dict(image=make_image_dict(updated_image)) - except exception.Invalid as e: - msg = (_("Failed to update image metadata. " - "Got error: %s") % encodeutils.exception_to_unicode(e)) - LOG.error(msg) - return exc.HTTPBadRequest(msg) - except exception.ImageNotFound: - LOG.info(_LI("Image %(id)s not found"), {'id': id}) - raise exc.HTTPNotFound(body='Image not found', - request=req, - content_type='text/plain') - except exception.ForbiddenPublicImage: - LOG.info(_LI("Update denied for public image %(id)s"), {'id': id}) - raise exc.HTTPForbidden() - except exception.Forbidden: - # If it's private and doesn't belong to them, don't let on - # that it exists - LOG.info(_LI("Access denied to image %(id)s but returning" - " 'not found'"), {'id': id}) - raise exc.HTTPNotFound(body='Image not found', - request=req, - content_type='text/plain') - except exception.Conflict as e: - LOG.info(encodeutils.exception_to_unicode(e)) - raise exc.HTTPConflict(body='Image operation conflicts', - request=req, - content_type='text/plain') - except Exception: - LOG.exception(_LE("Unable to update image %s"), id) - raise - - -def _limit_locations(image): - locations = image.pop('locations', []) - image['location_data'] = locations - image['location'] = None - for loc in locations: - if loc['status'] == 'active': - image['location'] = loc['url'] - break - - -def make_image_dict(image): - """Create a dict representation of an image which we can use to - serialize the image. - """ - - def _fetch_attrs(d, attrs): - return {a: d[a] for a in attrs if a in d.keys()} - - # TODO(sirp): should this be a dict, or a list of dicts? - # A plain dict is more convenient, but list of dicts would provide - # access to created_at, etc - properties = {p['name']: p['value'] for p in image['properties'] - if not p['deleted']} - - image_dict = _fetch_attrs(image, glance.db.IMAGE_ATTRS) - image_dict['properties'] = properties - _limit_locations(image_dict) - - return image_dict - - -def create_resource(): - """Images resource factory method.""" - deserializer = wsgi.JSONRequestDeserializer() - serializer = wsgi.JSONResponseSerializer() - return wsgi.Resource(Controller(), deserializer, serializer) diff --git a/glance/registry/api/v1/members.py b/glance/registry/api/v1/members.py deleted file mode 100644 index eba49ba29c..0000000000 --- a/glance/registry/api/v1/members.py +++ /dev/null @@ -1,366 +0,0 @@ -# Copyright 2010-2011 OpenStack Foundation -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from oslo_log import log as logging -from oslo_utils import encodeutils -import webob.exc - -from glance.common import exception -from glance.common import utils -from glance.common import wsgi -import glance.db -from glance.i18n import _, _LI, _LW - - -LOG = logging.getLogger(__name__) - - -class Controller(object): - - def _check_can_access_image_members(self, context): - if context.owner is None and not context.is_admin: - raise webob.exc.HTTPUnauthorized(_("No authenticated user")) - - def __init__(self): - self.db_api = glance.db.get_api() - - def is_image_sharable(self, context, image): - """Return True if the image can be shared to others in this context.""" - # Is admin == image sharable - if context.is_admin: - return True - - # Only allow sharing if we have an owner - if context.owner is None: - return False - - # If we own the image, we can share it - if context.owner == image['owner']: - return True - - members = self.db_api.image_member_find(context, - image_id=image['id'], - member=context.owner) - if members: - return members[0]['can_share'] - - return False - - def index(self, req, image_id): - """ - Get the members of an image. - """ - try: - self.db_api.image_get(req.context, image_id, v1_mode=True) - except exception.NotFound: - msg = _("Image %(id)s not found") % {'id': image_id} - LOG.warn(msg) - raise webob.exc.HTTPNotFound(msg) - except exception.Forbidden: - # If it's private and doesn't belong to them, don't let on - # that it exists - msg = _LW("Access denied to image %(id)s but returning" - " 'not found'") % {'id': image_id} - LOG.warn(msg) - raise webob.exc.HTTPNotFound() - - members = self.db_api.image_member_find(req.context, image_id=image_id) - LOG.debug("Returning member list for image %(id)s", {'id': image_id}) - return dict(members=make_member_list(members, - member_id='member', - can_share='can_share')) - - @utils.mutating - def update_all(self, req, image_id, body): - """ - Replaces the members of the image with those specified in the - body. The body is a dict with the following format:: - - {'memberships': [ - {'member_id': , - ['can_share': [True|False]]}, ... - ]} - """ - self._check_can_access_image_members(req.context) - - # Make sure the image exists - try: - image = self.db_api.image_get(req.context, image_id, v1_mode=True) - except exception.NotFound: - msg = _("Image %(id)s not found") % {'id': image_id} - LOG.warn(msg) - raise webob.exc.HTTPNotFound(msg) - except exception.Forbidden: - # If it's private and doesn't belong to them, don't let on - # that it exists - msg = _LW("Access denied to image %(id)s but returning" - " 'not found'") % {'id': image_id} - LOG.warn(msg) - raise webob.exc.HTTPNotFound() - - # Can they manipulate the membership? - if not self.is_image_sharable(req.context, image): - msg = (_LW("User lacks permission to share image %(id)s") % - {'id': image_id}) - LOG.warn(msg) - msg = _("No permission to share that image") - raise webob.exc.HTTPForbidden(msg) - - # Get the membership list - try: - memb_list = body['memberships'] - except Exception as e: - # Malformed entity... - msg = _LW("Invalid membership association specified for " - "image %(id)s") % {'id': image_id} - LOG.warn(msg) - msg = (_("Invalid membership association: %s") % - encodeutils.exception_to_unicode(e)) - raise webob.exc.HTTPBadRequest(explanation=msg) - - add = [] - existing = {} - # Walk through the incoming memberships - for memb in memb_list: - try: - datum = dict(image_id=image['id'], - member=memb['member_id'], - can_share=None) - except Exception as e: - # Malformed entity... - msg = _LW("Invalid membership association specified for " - "image %(id)s") % {'id': image_id} - LOG.warn(msg) - msg = (_("Invalid membership association: %s") % - encodeutils.exception_to_unicode(e)) - raise webob.exc.HTTPBadRequest(explanation=msg) - - # Figure out what can_share should be - if 'can_share' in memb: - datum['can_share'] = bool(memb['can_share']) - - # Try to find the corresponding membership - members = self.db_api.image_member_find(req.context, - image_id=datum['image_id'], - member=datum['member'], - include_deleted=True) - try: - member = members[0] - except IndexError: - # Default can_share - datum['can_share'] = bool(datum['can_share']) - add.append(datum) - else: - # Are we overriding can_share? - if datum['can_share'] is None: - datum['can_share'] = members[0]['can_share'] - - existing[member['id']] = { - 'values': datum, - 'membership': member, - } - - # We now have a filtered list of memberships to add and - # memberships to modify. Let's start by walking through all - # the existing image memberships... - existing_members = self.db_api.image_member_find(req.context, - image_id=image['id'], - include_deleted=True) - for member in existing_members: - if member['id'] in existing: - # Just update the membership in place - update = existing[member['id']]['values'] - self.db_api.image_member_update(req.context, - member['id'], - update) - else: - if not member['deleted']: - # Outdated one; needs to be deleted - self.db_api.image_member_delete(req.context, member['id']) - - # Now add the non-existent ones - for memb in add: - self.db_api.image_member_create(req.context, memb) - - # Make an appropriate result - LOG.info(_LI("Successfully updated memberships for image %(id)s"), - {'id': image_id}) - return webob.exc.HTTPNoContent() - - @utils.mutating - def update(self, req, image_id, id, body=None): - """ - Adds a membership to the image, or updates an existing one. - If a body is present, it is a dict with the following format:: - - {'member': { - 'can_share': [True|False] - }} - - If `can_share` is provided, the member's ability to share is - set accordingly. If it is not provided, existing memberships - remain unchanged and new memberships default to False. - """ - self._check_can_access_image_members(req.context) - - # Make sure the image exists - try: - image = self.db_api.image_get(req.context, image_id, v1_mode=True) - except exception.NotFound: - msg = _("Image %(id)s not found") % {'id': image_id} - LOG.warn(msg) - raise webob.exc.HTTPNotFound(msg) - except exception.Forbidden: - # If it's private and doesn't belong to them, don't let on - # that it exists - msg = _LW("Access denied to image %(id)s but returning" - " 'not found'") % {'id': image_id} - LOG.warn(msg) - raise webob.exc.HTTPNotFound() - - # Can they manipulate the membership? - if not self.is_image_sharable(req.context, image): - msg = (_LW("User lacks permission to share image %(id)s") % - {'id': image_id}) - LOG.warn(msg) - msg = _("No permission to share that image") - raise webob.exc.HTTPForbidden(msg) - - # Determine the applicable can_share value - can_share = None - if body: - try: - can_share = bool(body['member']['can_share']) - except Exception as e: - # Malformed entity... - msg = _LW("Invalid membership association specified for " - "image %(id)s") % {'id': image_id} - LOG.warn(msg) - msg = (_("Invalid membership association: %s") % - encodeutils.exception_to_unicode(e)) - raise webob.exc.HTTPBadRequest(explanation=msg) - - # Look up an existing membership... - members = self.db_api.image_member_find(req.context, - image_id=image_id, - member=id, - include_deleted=True) - if members: - if can_share is not None: - values = dict(can_share=can_share) - self.db_api.image_member_update(req.context, - members[0]['id'], - values) - else: - values = dict(image_id=image['id'], member=id, - can_share=bool(can_share)) - self.db_api.image_member_create(req.context, values) - - LOG.info(_LI("Successfully updated a membership for image %(id)s"), - {'id': image_id}) - return webob.exc.HTTPNoContent() - - @utils.mutating - def delete(self, req, image_id, id): - """ - Removes a membership from the image. - """ - self._check_can_access_image_members(req.context) - - # Make sure the image exists - try: - image = self.db_api.image_get(req.context, image_id, v1_mode=True) - except exception.NotFound: - msg = _("Image %(id)s not found") % {'id': image_id} - LOG.warn(msg) - raise webob.exc.HTTPNotFound(msg) - except exception.Forbidden: - # If it's private and doesn't belong to them, don't let on - # that it exists - msg = _LW("Access denied to image %(id)s but returning" - " 'not found'") % {'id': image_id} - LOG.warn(msg) - raise webob.exc.HTTPNotFound() - - # Can they manipulate the membership? - if not self.is_image_sharable(req.context, image): - msg = (_LW("User lacks permission to share image %(id)s") % - {'id': image_id}) - LOG.warn(msg) - msg = _("No permission to share that image") - raise webob.exc.HTTPForbidden(msg) - - # Look up an existing membership - members = self.db_api.image_member_find(req.context, - image_id=image_id, - member=id) - if members: - self.db_api.image_member_delete(req.context, members[0]['id']) - else: - LOG.debug("%(id)s is not a member of image %(image_id)s", - {'id': id, 'image_id': image_id}) - msg = _("Membership could not be found.") - raise webob.exc.HTTPNotFound(explanation=msg) - - # Make an appropriate result - LOG.info(_LI("Successfully deleted a membership from image %(id)s"), - {'id': image_id}) - return webob.exc.HTTPNoContent() - - def default(self, req, *args, **kwargs): - """This will cover the missing 'show' and 'create' actions""" - LOG.debug("The method %s is not allowed for this resource", - req.environ['REQUEST_METHOD']) - raise webob.exc.HTTPMethodNotAllowed( - headers=[('Allow', 'PUT, DELETE')]) - - def index_shared_images(self, req, id): - """ - Retrieves images shared with the given member. - """ - try: - members = self.db_api.image_member_find(req.context, member=id) - except exception.NotFound: - msg = _LW("Member %(id)s not found") % {'id': id} - LOG.warn(msg) - msg = _("Membership could not be found.") - raise webob.exc.HTTPBadRequest(explanation=msg) - - LOG.debug("Returning list of images shared with member %(id)s", - {'id': id}) - return dict(shared_images=make_member_list(members, - image_id='image_id', - can_share='can_share')) - - -def make_member_list(members, **attr_map): - """ - Create a dict representation of a list of members which we can use - to serialize the members list. Keyword arguments map the names of - optional attributes to include to the database attribute. - """ - - def _fetch_memb(memb, attr_map): - return {k: memb[v] for k, v in attr_map.items() if v in memb.keys()} - - # Return the list of members with the given attribute mapping - return [_fetch_memb(memb, attr_map) for memb in members] - - -def create_resource(): - """Image members resource factory method.""" - deserializer = wsgi.JSONRequestDeserializer() - serializer = wsgi.JSONResponseSerializer() - return wsgi.Resource(Controller(), deserializer, serializer) diff --git a/glance/registry/client/__init__.py b/glance/registry/client/__init__.py deleted file mode 100644 index d8ccece0fc..0000000000 --- a/glance/registry/client/__init__.py +++ /dev/null @@ -1,264 +0,0 @@ -# Copyright 2013 OpenStack Foundation -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from oslo_config import cfg - -from glance.i18n import _ - - -registry_client_opts = [ - cfg.StrOpt('registry_client_protocol', - default='http', - choices=('http', 'https'), - deprecated_for_removal=True, - deprecated_since="Queens", - deprecated_reason=_(""" -Glance registry service is deprecated for removal. - -More information can be found from the spec: -http://specs.openstack.org/openstack/glance-specs/specs/queens/approved/glance/deprecate-registry.html -"""), - help=_(""" -Protocol to use for communication with the registry server. - -Provide a string value representing the protocol to use for -communication with the registry server. By default, this option is -set to ``http`` and the connection is not secure. - -This option can be set to ``https`` to establish a secure connection -to the registry server. In this case, provide a key to use for the -SSL connection using the ``registry_client_key_file`` option. Also -include the CA file and cert file using the options -``registry_client_ca_file`` and ``registry_client_cert_file`` -respectively. - -Possible values: - * http - * https - -Related options: - * registry_client_key_file - * registry_client_cert_file - * registry_client_ca_file - -""")), - cfg.StrOpt('registry_client_key_file', - sample_default='/etc/ssl/key/key-file.pem', - deprecated_for_removal=True, - deprecated_since="Queens", - deprecated_reason=_(""" -Glance registry service is deprecated for removal. - -More information can be found from the spec: -http://specs.openstack.org/openstack/glance-specs/specs/queens/approved/glance/deprecate-registry.html -"""), - help=_(""" -Absolute path to the private key file. - -Provide a string value representing a valid absolute path to the -private key file to use for establishing a secure connection to -the registry server. - -NOTE: This option must be set if ``registry_client_protocol`` is -set to ``https``. Alternatively, the GLANCE_CLIENT_KEY_FILE -environment variable may be set to a filepath of the key file. - -Possible values: - * String value representing a valid absolute path to the key - file. - -Related options: - * registry_client_protocol - -""")), - cfg.StrOpt('registry_client_cert_file', - sample_default='/etc/ssl/certs/file.crt', - deprecated_for_removal=True, - deprecated_since="Queens", - deprecated_reason=_(""" -Glance registry service is deprecated for removal. - -More information can be found from the spec: -http://specs.openstack.org/openstack/glance-specs/specs/queens/approved/glance/deprecate-registry.html -"""), - help=_(""" -Absolute path to the certificate file. - -Provide a string value representing a valid absolute path to the -certificate file to use for establishing a secure connection to -the registry server. - -NOTE: This option must be set if ``registry_client_protocol`` is -set to ``https``. Alternatively, the GLANCE_CLIENT_CERT_FILE -environment variable may be set to a filepath of the certificate -file. - -Possible values: - * String value representing a valid absolute path to the - certificate file. - -Related options: - * registry_client_protocol - -""")), - cfg.StrOpt('registry_client_ca_file', - sample_default='/etc/ssl/cafile/file.ca', - deprecated_for_removal=True, - deprecated_since="Queens", - deprecated_reason=_(""" -Glance registry service is deprecated for removal. - -More information can be found from the spec: -http://specs.openstack.org/openstack/glance-specs/specs/queens/approved/glance/deprecate-registry.html -"""), - help=_(""" -Absolute path to the Certificate Authority file. - -Provide a string value representing a valid absolute path to the -certificate authority file to use for establishing a secure -connection to the registry server. - -NOTE: This option must be set if ``registry_client_protocol`` is -set to ``https``. Alternatively, the GLANCE_CLIENT_CA_FILE -environment variable may be set to a filepath of the CA file. -This option is ignored if the ``registry_client_insecure`` option -is set to ``True``. - -Possible values: - * String value representing a valid absolute path to the CA - file. - -Related options: - * registry_client_protocol - * registry_client_insecure - -""")), - cfg.BoolOpt('registry_client_insecure', - default=False, - deprecated_for_removal=True, - deprecated_since="Queens", - deprecated_reason=_(""" -Glance registry service is deprecated for removal. - -More information can be found from the spec: -http://specs.openstack.org/openstack/glance-specs/specs/queens/approved/glance/deprecate-registry.html -"""), - help=_(""" -Set verification of the registry server certificate. - -Provide a boolean value to determine whether or not to validate -SSL connections to the registry server. By default, this option -is set to ``False`` and the SSL connections are validated. - -If set to ``True``, the connection to the registry server is not -validated via a certifying authority and the -``registry_client_ca_file`` option is ignored. This is the -registry's equivalent of specifying --insecure on the command line -using glanceclient for the API. - -Possible values: - * True - * False - -Related options: - * registry_client_protocol - * registry_client_ca_file - -""")), - cfg.IntOpt('registry_client_timeout', - default=600, - min=0, - deprecated_for_removal=True, - deprecated_since="Queens", - deprecated_reason=_(""" -Glance registry service is deprecated for removal. - -More information can be found from the spec: -http://specs.openstack.org/openstack/glance-specs/specs/queens/approved/glance/deprecate-registry.html -"""), - help=_(""" -Timeout value for registry requests. - -Provide an integer value representing the period of time in seconds -that the API server will wait for a registry request to complete. -The default value is 600 seconds. - -A value of 0 implies that a request will never timeout. - -Possible values: - * Zero - * Positive integer - -Related options: - * None - -""")), -] - -_DEPRECATE_USE_USER_TOKEN_MSG = ('This option was considered harmful and ' - 'has been deprecated in M release. It will ' - 'be removed in O release. For more ' - 'information read OSSN-0060. ' - 'Related functionality with uploading big ' - 'images has been implemented with Keystone ' - 'trusts support.') - -registry_client_ctx_opts = [ - cfg.BoolOpt('use_user_token', default=True, deprecated_for_removal=True, - deprecated_reason=_DEPRECATE_USE_USER_TOKEN_MSG, - help=_('Whether to pass through the user token when ' - 'making requests to the registry. To prevent ' - 'failures with token expiration during big ' - 'files upload, it is recommended to set this ' - 'parameter to False.' - 'If "use_user_token" is not in effect, then ' - 'admin credentials can be specified.')), - cfg.StrOpt('admin_user', secret=True, deprecated_for_removal=True, - deprecated_reason=_DEPRECATE_USE_USER_TOKEN_MSG, - help=_('The administrators user name. ' - 'If "use_user_token" is not in effect, then ' - 'admin credentials can be specified.')), - cfg.StrOpt('admin_password', secret=True, deprecated_for_removal=True, - deprecated_reason=_DEPRECATE_USE_USER_TOKEN_MSG, - help=_('The administrators password. ' - 'If "use_user_token" is not in effect, then ' - 'admin credentials can be specified.')), - cfg.StrOpt('admin_tenant_name', secret=True, deprecated_for_removal=True, - deprecated_reason=_DEPRECATE_USE_USER_TOKEN_MSG, - help=_('The tenant name of the administrative user. ' - 'If "use_user_token" is not in effect, then ' - 'admin tenant name can be specified.')), - cfg.StrOpt('auth_url', deprecated_for_removal=True, - deprecated_reason=_DEPRECATE_USE_USER_TOKEN_MSG, - help=_('The URL to the keystone service. ' - 'If "use_user_token" is not in effect and ' - 'using keystone auth, then URL of keystone ' - 'can be specified.')), - cfg.StrOpt('auth_strategy', default='noauth', deprecated_for_removal=True, - deprecated_reason=_DEPRECATE_USE_USER_TOKEN_MSG, - help=_('The strategy to use for authentication. ' - 'If "use_user_token" is not in effect, then ' - 'auth strategy can be specified.')), - cfg.StrOpt('auth_region', deprecated_for_removal=True, - deprecated_reason=_DEPRECATE_USE_USER_TOKEN_MSG, - help=_('The region for the authentication service. ' - 'If "use_user_token" is not in effect and ' - 'using keystone auth, then region name can ' - 'be specified.')), -] - -CONF = cfg.CONF -CONF.register_opts(registry_client_opts) -CONF.register_opts(registry_client_ctx_opts) diff --git a/glance/registry/client/v1/__init__.py b/glance/registry/client/v1/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/glance/registry/client/v1/api.py b/glance/registry/client/v1/api.py deleted file mode 100644 index 5cbc1275e9..0000000000 --- a/glance/registry/client/v1/api.py +++ /dev/null @@ -1,227 +0,0 @@ -# Copyright 2010-2011 OpenStack Foundation -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -""" -Registry's Client API -""" - -import os - -from oslo_config import cfg -from oslo_log import log as logging -from oslo_serialization import jsonutils - -from glance.common import exception -from glance.i18n import _ -from glance.registry.client.v1 import client - -LOG = logging.getLogger(__name__) - -registry_client_ctx_opts = [ - cfg.BoolOpt('send_identity_headers', - default=False, - help=_(""" -Send headers received from identity when making requests to -registry. - -Typically, Glance registry can be deployed in multiple flavors, -which may or may not include authentication. For example, -``trusted-auth`` is a flavor that does not require the registry -service to authenticate the requests it receives. However, the -registry service may still need a user context to be populated to -serve the requests. This can be achieved by the caller -(the Glance API usually) passing through the headers it received -from authenticating with identity for the same request. The typical -headers sent are ``X-User-Id``, ``X-Tenant-Id``, ``X-Roles``, -``X-Identity-Status`` and ``X-Service-Catalog``. - -Provide a boolean value to determine whether to send the identity -headers to provide tenant and user information along with the -requests to registry service. By default, this option is set to -``False``, which means that user and tenant information is not -available readily. It must be obtained by authenticating. Hence, if -this is set to ``False``, ``flavor`` must be set to value that -either includes authentication or authenticated user context. - -Possible values: - * True - * False - -Related options: - * flavor - -""")), -] - -CONF = cfg.CONF -CONF.register_opts(registry_client_ctx_opts) -_registry_client = 'glance.registry.client' -CONF.import_opt('registry_client_protocol', _registry_client) -CONF.import_opt('registry_client_key_file', _registry_client) -CONF.import_opt('registry_client_cert_file', _registry_client) -CONF.import_opt('registry_client_ca_file', _registry_client) -CONF.import_opt('registry_client_insecure', _registry_client) -CONF.import_opt('registry_client_timeout', _registry_client) -CONF.import_opt('use_user_token', _registry_client) -CONF.import_opt('admin_user', _registry_client) -CONF.import_opt('admin_password', _registry_client) -CONF.import_opt('admin_tenant_name', _registry_client) -CONF.import_opt('auth_url', _registry_client) -CONF.import_opt('auth_strategy', _registry_client) -CONF.import_opt('auth_region', _registry_client) -CONF.import_opt('metadata_encryption_key', 'glance.common.config') - -_CLIENT_CREDS = None -_CLIENT_HOST = None -_CLIENT_PORT = None -_CLIENT_KWARGS = {} -# AES key used to encrypt 'location' metadata -_METADATA_ENCRYPTION_KEY = None - - -def configure_registry_client(): - """ - Sets up a registry client for use in registry lookups - """ - global _CLIENT_KWARGS, _CLIENT_HOST, _CLIENT_PORT, _METADATA_ENCRYPTION_KEY - try: - host, port = CONF.registry_host, CONF.registry_port - except cfg.ConfigFileValueError: - msg = _("Configuration option was not valid") - LOG.error(msg) - raise exception.BadRegistryConnectionConfiguration(reason=msg) - except IndexError: - msg = _("Could not find required configuration option") - LOG.error(msg) - raise exception.BadRegistryConnectionConfiguration(reason=msg) - - _CLIENT_HOST = host - _CLIENT_PORT = port - _METADATA_ENCRYPTION_KEY = CONF.metadata_encryption_key - _CLIENT_KWARGS = { - 'use_ssl': CONF.registry_client_protocol.lower() == 'https', - 'key_file': CONF.registry_client_key_file, - 'cert_file': CONF.registry_client_cert_file, - 'ca_file': CONF.registry_client_ca_file, - 'insecure': CONF.registry_client_insecure, - 'timeout': CONF.registry_client_timeout, - } - - if not CONF.use_user_token: - configure_registry_admin_creds() - - -def configure_registry_admin_creds(): - global _CLIENT_CREDS - - if CONF.auth_url or os.getenv('OS_AUTH_URL'): - strategy = 'keystone' - else: - strategy = CONF.auth_strategy - - _CLIENT_CREDS = { - 'user': CONF.admin_user, - 'password': CONF.admin_password, - 'username': CONF.admin_user, - 'tenant': CONF.admin_tenant_name, - 'auth_url': os.getenv('OS_AUTH_URL') or CONF.auth_url, - 'strategy': strategy, - 'region': CONF.auth_region, - } - - -def get_registry_client(cxt): - global _CLIENT_CREDS, _CLIENT_KWARGS, _CLIENT_HOST, _CLIENT_PORT - global _METADATA_ENCRYPTION_KEY - kwargs = _CLIENT_KWARGS.copy() - if CONF.use_user_token: - kwargs['auth_token'] = cxt.auth_token - if _CLIENT_CREDS: - kwargs['creds'] = _CLIENT_CREDS - - if CONF.send_identity_headers: - identity_headers = { - 'X-User-Id': cxt.user_id or '', - 'X-Tenant-Id': cxt.project_id or '', - 'X-Roles': ','.join(cxt.roles), - 'X-Identity-Status': 'Confirmed', - 'X-Service-Catalog': jsonutils.dumps(cxt.service_catalog), - } - kwargs['identity_headers'] = identity_headers - - kwargs['request_id'] = cxt.request_id - - return client.RegistryClient(_CLIENT_HOST, _CLIENT_PORT, - _METADATA_ENCRYPTION_KEY, **kwargs) - - -def get_images_list(context, **kwargs): - c = get_registry_client(context) - return c.get_images(**kwargs) - - -def get_images_detail(context, **kwargs): - c = get_registry_client(context) - return c.get_images_detailed(**kwargs) - - -def get_image_metadata(context, image_id): - c = get_registry_client(context) - return c.get_image(image_id) - - -def add_image_metadata(context, image_meta): - LOG.debug("Adding image metadata...") - c = get_registry_client(context) - return c.add_image(image_meta) - - -def update_image_metadata(context, image_id, image_meta, - purge_props=False, from_state=None): - LOG.debug("Updating image metadata for image %s...", image_id) - c = get_registry_client(context) - return c.update_image(image_id, image_meta, purge_props=purge_props, - from_state=from_state) - - -def delete_image_metadata(context, image_id): - LOG.debug("Deleting image metadata for image %s...", image_id) - c = get_registry_client(context) - return c.delete_image(image_id) - - -def get_image_members(context, image_id): - c = get_registry_client(context) - return c.get_image_members(image_id) - - -def get_member_images(context, member_id): - c = get_registry_client(context) - return c.get_member_images(member_id) - - -def replace_members(context, image_id, member_data): - c = get_registry_client(context) - return c.replace_members(image_id, member_data) - - -def add_member(context, image_id, member_id, can_share=None): - c = get_registry_client(context) - return c.add_member(image_id, member_id, can_share=can_share) - - -def delete_member(context, image_id, member_id): - c = get_registry_client(context) - return c.delete_member(image_id, member_id) diff --git a/glance/registry/client/v1/client.py b/glance/registry/client/v1/client.py deleted file mode 100644 index 45c2fb2b35..0000000000 --- a/glance/registry/client/v1/client.py +++ /dev/null @@ -1,276 +0,0 @@ -# Copyright 2013 OpenStack Foundation -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -""" -Simple client class to speak with any RESTful service that implements -the Glance Registry API -""" - -from oslo_log import log as logging -from oslo_serialization import jsonutils -from oslo_utils import excutils -import six - -from glance.common.client import BaseClient -from glance.common import crypt -from glance.common import exception -from glance.i18n import _LE -from glance.registry.api.v1 import images - -LOG = logging.getLogger(__name__) - - -class RegistryClient(BaseClient): - - """A client for the Registry image metadata service.""" - - DEFAULT_PORT = 9191 - - def __init__(self, host=None, port=None, metadata_encryption_key=None, - identity_headers=None, **kwargs): - """ - :param metadata_encryption_key: Key used to encrypt 'location' metadata - """ - self.metadata_encryption_key = metadata_encryption_key - # NOTE (dprince): by default base client overwrites host and port - # settings when using keystone. configure_via_auth=False disables - # this behaviour to ensure we still send requests to the Registry API - self.identity_headers = identity_headers - # store available passed request id for do_request call - self._passed_request_id = kwargs.pop('request_id', None) - BaseClient.__init__(self, host, port, configure_via_auth=False, - **kwargs) - - def decrypt_metadata(self, image_metadata): - if self.metadata_encryption_key: - if image_metadata.get('location'): - location = crypt.urlsafe_decrypt(self.metadata_encryption_key, - image_metadata['location']) - image_metadata['location'] = location - if image_metadata.get('location_data'): - ld = [] - for loc in image_metadata['location_data']: - url = crypt.urlsafe_decrypt(self.metadata_encryption_key, - loc['url']) - ld.append({'id': loc['id'], 'url': url, - 'metadata': loc['metadata'], - 'status': loc['status']}) - image_metadata['location_data'] = ld - return image_metadata - - def encrypt_metadata(self, image_metadata): - if self.metadata_encryption_key: - location_url = image_metadata.get('location') - if location_url: - location = crypt.urlsafe_encrypt(self.metadata_encryption_key, - location_url, - 64) - image_metadata['location'] = location - if image_metadata.get('location_data'): - ld = [] - for loc in image_metadata['location_data']: - if loc['url'] == location_url: - url = location - else: - url = crypt.urlsafe_encrypt( - self.metadata_encryption_key, loc['url'], 64) - ld.append({'url': url, 'metadata': loc['metadata'], - 'status': loc['status'], - # NOTE(zhiyan): New location has no ID field. - 'id': loc.get('id')}) - image_metadata['location_data'] = ld - return image_metadata - - def get_images(self, **kwargs): - """ - Returns a list of image id/name mappings from Registry - - :param filters: dict of keys & expected values to filter results - :param marker: image id after which to start page - :param limit: max number of images to return - :param sort_key: results will be ordered by this image attribute - :param sort_dir: direction in which to order results (asc, desc) - """ - params = self._extract_params(kwargs, images.SUPPORTED_PARAMS) - res = self.do_request("GET", "/images", params=params) - image_list = jsonutils.loads(res.read())['images'] - for image in image_list: - image = self.decrypt_metadata(image) - return image_list - - def do_request(self, method, action, **kwargs): - try: - kwargs['headers'] = kwargs.get('headers', {}) - kwargs['headers'].update(self.identity_headers or {}) - if self._passed_request_id: - request_id = self._passed_request_id - if six.PY3 and isinstance(request_id, bytes): - request_id = request_id.decode('utf-8') - kwargs['headers']['X-Openstack-Request-ID'] = request_id - res = super(RegistryClient, self).do_request(method, - action, - **kwargs) - status = res.status - request_id = res.getheader('x-openstack-request-id') - if six.PY3 and isinstance(request_id, bytes): - request_id = request_id.decode('utf-8') - LOG.debug("Registry request %(method)s %(action)s HTTP %(status)s" - " request id %(request_id)s", - {'method': method, 'action': action, - 'status': status, 'request_id': request_id}) - - # a 404 condition is not fatal, we shouldn't log at a fatal - # level for it. - except exception.NotFound: - raise - - # The following exception logging should only really be used - # in extreme and unexpected cases. - except Exception as exc: - with excutils.save_and_reraise_exception(): - exc_name = exc.__class__.__name__ - LOG.exception(_LE("Registry client request %(method)s " - "%(action)s raised %(exc_name)s"), - {'method': method, 'action': action, - 'exc_name': exc_name}) - return res - - def get_images_detailed(self, **kwargs): - """ - Returns a list of detailed image data mappings from Registry - - :param filters: dict of keys & expected values to filter results - :param marker: image id after which to start page - :param limit: max number of images to return - :param sort_key: results will be ordered by this image attribute - :param sort_dir: direction in which to order results (asc, desc) - """ - params = self._extract_params(kwargs, images.SUPPORTED_PARAMS) - res = self.do_request("GET", "/images/detail", params=params) - image_list = jsonutils.loads(res.read())['images'] - for image in image_list: - image = self.decrypt_metadata(image) - return image_list - - def get_image(self, image_id): - """Returns a mapping of image metadata from Registry.""" - res = self.do_request("GET", "/images/%s" % image_id) - data = jsonutils.loads(res.read())['image'] - return self.decrypt_metadata(data) - - def add_image(self, image_metadata): - """ - Tells registry about an image's metadata - """ - headers = { - 'Content-Type': 'application/json', - } - - if 'image' not in image_metadata: - image_metadata = dict(image=image_metadata) - - encrypted_metadata = self.encrypt_metadata(image_metadata['image']) - image_metadata['image'] = encrypted_metadata - body = jsonutils.dump_as_bytes(image_metadata) - - res = self.do_request("POST", "/images", body=body, headers=headers) - # Registry returns a JSONified dict(image=image_info) - data = jsonutils.loads(res.read()) - image = data['image'] - return self.decrypt_metadata(image) - - def update_image(self, image_id, image_metadata, purge_props=False, - from_state=None): - """ - Updates Registry's information about an image - """ - if 'image' not in image_metadata: - image_metadata = dict(image=image_metadata) - - encrypted_metadata = self.encrypt_metadata(image_metadata['image']) - image_metadata['image'] = encrypted_metadata - image_metadata['from_state'] = from_state - body = jsonutils.dump_as_bytes(image_metadata) - - headers = { - 'Content-Type': 'application/json', - } - - if purge_props: - headers["X-Glance-Registry-Purge-Props"] = "true" - - res = self.do_request("PUT", "/images/%s" % image_id, body=body, - headers=headers) - data = jsonutils.loads(res.read()) - image = data['image'] - return self.decrypt_metadata(image) - - def delete_image(self, image_id): - """ - Deletes Registry's information about an image - """ - res = self.do_request("DELETE", "/images/%s" % image_id) - data = jsonutils.loads(res.read()) - image = data['image'] - return image - - def get_image_members(self, image_id): - """Return a list of membership associations from Registry.""" - res = self.do_request("GET", "/images/%s/members" % image_id) - data = jsonutils.loads(res.read())['members'] - return data - - def get_member_images(self, member_id): - """Return a list of membership associations from Registry.""" - res = self.do_request("GET", "/shared-images/%s" % member_id) - data = jsonutils.loads(res.read())['shared_images'] - return data - - def replace_members(self, image_id, member_data): - """Replace registry's information about image membership.""" - if isinstance(member_data, (list, tuple)): - member_data = dict(memberships=list(member_data)) - elif (isinstance(member_data, dict) and - 'memberships' not in member_data): - member_data = dict(memberships=[member_data]) - - body = jsonutils.dump_as_bytes(member_data) - - headers = {'Content-Type': 'application/json', } - - res = self.do_request("PUT", "/images/%s/members" % image_id, - body=body, headers=headers) - return self.get_status_code(res) == 204 - - def add_member(self, image_id, member_id, can_share=None): - """Add to registry's information about image membership.""" - body = None - headers = {} - # Build up a body if can_share is specified - if can_share is not None: - body = jsonutils.dump_as_bytes( - dict(member=dict(can_share=can_share))) - headers['Content-Type'] = 'application/json' - - url = "/images/%s/members/%s" % (image_id, member_id) - res = self.do_request("PUT", url, body=body, - headers=headers) - return self.get_status_code(res) == 204 - - def delete_member(self, image_id, member_id): - """Delete registry's information about image membership.""" - res = self.do_request("DELETE", "/images/%s/members/%s" % - (image_id, member_id)) - return self.get_status_code(res) == 204 diff --git a/glance/registry/client/v2/__init__.py b/glance/registry/client/v2/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/glance/registry/client/v2/api.py b/glance/registry/client/v2/api.py deleted file mode 100644 index 0a2397ebb9..0000000000 --- a/glance/registry/client/v2/api.py +++ /dev/null @@ -1,109 +0,0 @@ -# Copyright 2013 Red Hat, Inc -# 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. - -""" -Registry's Client V2 -""" - -import os - -from oslo_config import cfg -from oslo_log import log as logging - -from glance.common import exception -from glance.i18n import _ -from glance.registry.client.v2 import client - -LOG = logging.getLogger(__name__) - -CONF = cfg.CONF -_registry_client = 'glance.registry.client' -CONF.import_opt('registry_client_protocol', _registry_client) -CONF.import_opt('registry_client_key_file', _registry_client) -CONF.import_opt('registry_client_ca_file', _registry_client) -CONF.import_opt('registry_client_insecure', _registry_client) -CONF.import_opt('registry_client_timeout', _registry_client) -CONF.import_opt('use_user_token', _registry_client) -CONF.import_opt('admin_user', _registry_client) -CONF.import_opt('admin_password', _registry_client) -CONF.import_opt('admin_tenant_name', _registry_client) -CONF.import_opt('auth_url', _registry_client) -CONF.import_opt('auth_strategy', _registry_client) -CONF.import_opt('auth_region', _registry_client) - -_CLIENT_CREDS = None -_CLIENT_HOST = None -_CLIENT_PORT = None -_CLIENT_KWARGS = {} - - -def configure_registry_client(): - """ - Sets up a registry client for use in registry lookups - """ - global _CLIENT_KWARGS, _CLIENT_HOST, _CLIENT_PORT - try: - host, port = CONF.registry_host, CONF.registry_port - except cfg.ConfigFileValueError: - msg = _("Configuration option was not valid") - LOG.error(msg) - raise exception.BadRegistryConnectionConfiguration(msg) - except IndexError: - msg = _("Could not find required configuration option") - LOG.error(msg) - raise exception.BadRegistryConnectionConfiguration(msg) - - _CLIENT_HOST = host - _CLIENT_PORT = port - _CLIENT_KWARGS = { - 'use_ssl': CONF.registry_client_protocol.lower() == 'https', - 'key_file': CONF.registry_client_key_file, - 'cert_file': CONF.registry_client_cert_file, - 'ca_file': CONF.registry_client_ca_file, - 'insecure': CONF.registry_client_insecure, - 'timeout': CONF.registry_client_timeout, - } - - if not CONF.use_user_token: - configure_registry_admin_creds() - - -def configure_registry_admin_creds(): - global _CLIENT_CREDS - - if CONF.auth_url or os.getenv('OS_AUTH_URL'): - strategy = 'keystone' - else: - strategy = CONF.auth_strategy - - _CLIENT_CREDS = { - 'user': CONF.admin_user, - 'password': CONF.admin_password, - 'username': CONF.admin_user, - 'tenant': CONF.admin_tenant_name, - 'auth_url': os.getenv('OS_AUTH_URL') or CONF.auth_url, - 'strategy': strategy, - 'region': CONF.auth_region, - } - - -def get_registry_client(cxt): - global _CLIENT_CREDS, _CLIENT_KWARGS, _CLIENT_HOST, _CLIENT_PORT - kwargs = _CLIENT_KWARGS.copy() - if CONF.use_user_token: - kwargs['auth_token'] = cxt.auth_token - if _CLIENT_CREDS: - kwargs['creds'] = _CLIENT_CREDS - return client.RegistryClient(_CLIENT_HOST, _CLIENT_PORT, **kwargs) diff --git a/glance/registry/client/v2/client.py b/glance/registry/client/v2/client.py deleted file mode 100644 index c6d61a7cd6..0000000000 --- a/glance/registry/client/v2/client.py +++ /dev/null @@ -1,27 +0,0 @@ -# Copyright 2013 Red Hat, Inc. -# 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. - -""" -Simple client class to speak with any RESTful service that implements -the Glance Registry API -""" - -from glance.common import rpc - - -class RegistryClient(rpc.RPCClient): - """Registry's V2 Client.""" - - DEFAULT_PORT = 9191 diff --git a/glance/tests/functional/__init__.py b/glance/tests/functional/__init__.py index 50f11778b2..6506ad6939 100644 --- a/glance/tests/functional/__init__.py +++ b/glance/tests/functional/__init__.py @@ -482,15 +482,11 @@ pipeline = cors healthcheck versionnegotiation gzip context rootapp [composite:rootapp] paste.composite_factory = glance.api:root_app_factory /: apiversions -/v1: apiv1app /v2: apiv2app [app:apiversions] paste.app_factory = glance.api.versions:create_resource -[app:apiv1app] -paste.app_factory = glance.api.v1.router:API.factory - [app:apiv2app] paste.app_factory = glance.api.v2.router:API.factory @@ -663,15 +659,11 @@ pipeline = cors healthcheck versionnegotiation gzip context rootapp [composite:rootapp] paste.composite_factory = glance.api:root_app_factory /: apiversions -/v1: apiv1app /v2: apiv2app [app:apiversions] paste.app_factory = glance.api.versions:create_resource -[app:apiv1app] -paste.app_factory = glance.api.v1.router:API.factory - [app:apiv2app] paste.app_factory = glance.api.v2.router:API.factory diff --git a/glance/tests/integration/v2/base.py b/glance/tests/integration/v2/base.py index b851d21dce..b9580c05a6 100644 --- a/glance/tests/integration/v2/base.py +++ b/glance/tests/integration/v2/base.py @@ -55,15 +55,11 @@ pipeline = versionnegotiation gzip context rootapp [composite:rootapp] paste.composite_factory = glance.api:root_app_factory /: apiversions -/v1: apiv1app /v2: apiv2app [app:apiversions] paste.app_factory = glance.api.versions:create_resource -[app:apiv1app] -paste.app_factory = glance.api.v1.router:API.factory - [app:apiv2app] paste.app_factory = glance.api.v2.router:API.factory diff --git a/glance/tests/integration/v2/test_property_quota_violations.py b/glance/tests/integration/v2/test_property_quota_violations.py index b955009fa0..107580ce66 100644 --- a/glance/tests/integration/v2/test_property_quota_violations.py +++ b/glance/tests/integration/v2/test_property_quota_violations.py @@ -28,7 +28,6 @@ class TestPropertyQuotaViolations(base.ApiTest): def __init__(self, *args, **kwargs): super(TestPropertyQuotaViolations, self).__init__(*args, **kwargs) self.api_flavor = 'noauth' - self.registry_flavor = 'fakeauth' def _headers(self, custom_headers=None): base_headers = { diff --git a/glance/tests/integration/v2/test_tasks_api.py b/glance/tests/integration/v2/test_tasks_api.py index 9bb5f9a9ba..7185a48379 100644 --- a/glance/tests/integration/v2/test_tasks_api.py +++ b/glance/tests/integration/v2/test_tasks_api.py @@ -56,7 +56,6 @@ class TestTasksApi(base.ApiTest): def __init__(self, *args, **kwargs): super(TestTasksApi, self).__init__(*args, **kwargs) self.api_flavor = 'fakeauth' - self.registry_flavor = 'fakeauth' def _wait_on_task_execution(self, max_wait=5): """Wait until all the tasks have finished execution and are in diff --git a/glance/tests/stubs.py b/glance/tests/stubs.py index 41551b8a8f..e7159d6681 100644 --- a/glance/tests/stubs.py +++ b/glance/tests/stubs.py @@ -73,12 +73,8 @@ def stub_out_store_server(stubs, base_dir, **kwargs): def close(self): return True - def _clean_url(self, url): - # TODO(bcwaldon): Fix the hack that strips off v1 - return url.replace('/v1', '', 1) if url.startswith('/v1') else url - def putrequest(self, method, url): - self.req = webob.Request.blank(self._clean_url(url)) + self.req = webob.Request.blank(url) if self.stub_force_sendfile: fake_sendfile = FakeSendFile(self.req) stubs.Set(sendfile, 'sendfile', fake_sendfile.sendfile) @@ -100,7 +96,7 @@ def stub_out_store_server(stubs, base_dir, **kwargs): self.req.body += data.split("\r\n")[1] def request(self, method, url, body=None, headers=None): - self.req = webob.Request.blank(self._clean_url(url)) + self.req = webob.Request.blank(url) self.req.method = method if headers: self.req.headers = headers diff --git a/glance/tests/unit/common/test_rpc.py b/glance/tests/unit/common/test_rpc.py deleted file mode 100644 index 81f57aa3e7..0000000000 --- a/glance/tests/unit/common/test_rpc.py +++ /dev/null @@ -1,358 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright 2013 Red Hat, Inc. -# 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 - -from oslo_serialization import jsonutils -from oslo_utils import encodeutils -import routes -import six -from six.moves import http_client as http -import webob - -from glance.common import exception -from glance.common import rpc -from glance.common import wsgi -from glance.tests.unit import base -from glance.tests import utils as test_utils - - -class FakeResource(object): - """ - Fake resource defining some methods that - will be called later by the api. - """ - - def get_images(self, context, keyword=None): - return keyword - - def count_images(self, context, images): - return len(images) - - def get_all_images(self, context): - return False - - def raise_value_error(self, context): - raise ValueError("Yep, Just like that!") - - def raise_weird_error(self, context): - class WeirdError(Exception): - pass - raise WeirdError("Weirdness") - - -def create_api(): - deserializer = rpc.RPCJSONDeserializer() - serializer = rpc.RPCJSONSerializer() - controller = rpc.Controller() - controller.register(FakeResource()) - res = wsgi.Resource(controller, deserializer, serializer) - - mapper = routes.Mapper() - mapper.connect("/rpc", controller=res, - conditions=dict(method=["POST"]), - action="__call__") - return test_utils.FakeAuthMiddleware(wsgi.Router(mapper), is_admin=True) - - -class TestRPCController(base.IsolatedUnitTest): - - def setUp(self): - super(TestRPCController, self).setUp() - self.res = FakeResource() - self.controller = rpc.Controller() - self.controller.register(self.res) - - def test_register(self): - res = FakeResource() - controller = rpc.Controller() - controller.register(res) - self.assertIn("get_images", controller._registered) - self.assertIn("get_all_images", controller._registered) - - def test_reigster_filtered(self): - res = FakeResource() - controller = rpc.Controller() - controller.register(res, filtered=["get_all_images"]) - self.assertIn("get_all_images", controller._registered) - - def test_reigster_excluded(self): - res = FakeResource() - controller = rpc.Controller() - controller.register(res, excluded=["get_all_images"]) - self.assertIn("get_images", controller._registered) - - def test_reigster_refiner(self): - res = FakeResource() - controller = rpc.Controller() - - # Not callable - self.assertRaises(TypeError, - controller.register, - res, refiner="get_all_images") - - # Filter returns False - controller.register(res, refiner=lambda x: False) - self.assertNotIn("get_images", controller._registered) - self.assertNotIn("get_images", controller._registered) - - # Filter returns True - controller.register(res, refiner=lambda x: True) - self.assertIn("get_images", controller._registered) - self.assertIn("get_images", controller._registered) - - def test_request(self): - api = create_api() - req = webob.Request.blank('/rpc') - req.method = 'POST' - req.body = jsonutils.dump_as_bytes([ - { - "command": "get_images", - "kwargs": {"keyword": 1} - } - ]) - res = req.get_response(api) - returned = jsonutils.loads(res.body) - self.assertIsInstance(returned, list) - self.assertEqual(1, returned[0]) - - def test_request_exc(self): - api = create_api() - req = webob.Request.blank('/rpc') - req.method = 'POST' - req.body = jsonutils.dump_as_bytes([ - { - "command": "get_all_images", - "kwargs": {"keyword": 1} - } - ]) - - # Sending non-accepted keyword - # to get_all_images method - res = req.get_response(api) - returned = jsonutils.loads(res.body) - self.assertIn("_error", returned[0]) - - def test_rpc_errors(self): - api = create_api() - req = webob.Request.blank('/rpc') - req.method = 'POST' - req.content_type = 'application/json' - - # Body is not a list, it should fail - req.body = jsonutils.dump_as_bytes({}) - res = req.get_response(api) - self.assertEqual(http.BAD_REQUEST, res.status_int) - - # cmd is not dict, it should fail. - req.body = jsonutils.dump_as_bytes([None]) - res = req.get_response(api) - self.assertEqual(http.BAD_REQUEST, res.status_int) - - # No command key, it should fail. - req.body = jsonutils.dump_as_bytes([{}]) - res = req.get_response(api) - self.assertEqual(http.BAD_REQUEST, res.status_int) - - # kwargs not dict, it should fail. - req.body = jsonutils.dump_as_bytes([{"command": "test", "kwargs": 2}]) - res = req.get_response(api) - self.assertEqual(http.BAD_REQUEST, res.status_int) - - # Command does not exist, it should fail. - req.body = jsonutils.dump_as_bytes([{"command": "test"}]) - res = req.get_response(api) - self.assertEqual(http.NOT_FOUND, res.status_int) - - def test_rpc_exception_propagation(self): - api = create_api() - req = webob.Request.blank('/rpc') - req.method = 'POST' - req.content_type = 'application/json' - - req.body = jsonutils.dump_as_bytes([{"command": "raise_value_error"}]) - res = req.get_response(api) - self.assertEqual(http.OK, res.status_int) - - returned = jsonutils.loads(res.body)[0] - err_cls = 'builtins.ValueError' if six.PY3 else 'exceptions.ValueError' - self.assertEqual(err_cls, returned['_error']['cls']) - - req.body = jsonutils.dump_as_bytes([{"command": "raise_weird_error"}]) - res = req.get_response(api) - self.assertEqual(http.OK, res.status_int) - - returned = jsonutils.loads(res.body)[0] - self.assertEqual('glance.common.exception.RPCError', - returned['_error']['cls']) - - -class TestRPCClient(base.IsolatedUnitTest): - - def setUp(self): - super(TestRPCClient, self).setUp() - self.api = create_api() - self.client = rpc.RPCClient(host="http://127.0.0.1:9191") - self.client._do_request = self.fake_request - - def fake_request(self, method, url, body, headers): - req = webob.Request.blank(url.path) - body = encodeutils.to_utf8(body) - req.body = body - req.method = method - - webob_res = req.get_response(self.api) - return test_utils.FakeHTTPResponse(status=webob_res.status_int, - headers=webob_res.headers, - data=webob_res.body) - - def test_method_proxy(self): - proxy = self.client.some_method - self.assertIn("method_proxy", str(proxy)) - - def test_bulk_request(self): - commands = [{"command": "get_images", 'kwargs': {'keyword': True}}, - {"command": "get_all_images"}] - - res = self.client.bulk_request(commands) - self.assertEqual(2, len(res)) - self.assertTrue(res[0]) - self.assertFalse(res[1]) - - def test_exception_raise(self): - try: - self.client.raise_value_error() - self.fail("Exception not raised") - except ValueError as exc: - self.assertEqual("Yep, Just like that!", str(exc)) - - def test_rpc_exception(self): - try: - self.client.raise_weird_error() - self.fail("Exception not raised") - except exception.RPCError: - pass - - def test_non_str_or_dict_response(self): - rst = self.client.count_images(images=[1, 2, 3, 4]) - self.assertEqual(4, rst) - self.assertIsInstance(rst, int) - - -class TestRPCJSONSerializer(test_utils.BaseTestCase): - - def test_to_json(self): - fixture = {"key": "value"} - expected = b'{"key": "value"}' - actual = rpc.RPCJSONSerializer().to_json(fixture) - self.assertEqual(expected, actual) - - def test_to_json_with_date_format_value(self): - fixture = {"date": datetime.datetime(1900, 3, 8, 2)} - expected = {"date": {"_value": "1900-03-08T02:00:00", - "_type": "datetime"}} - actual = rpc.RPCJSONSerializer().to_json(fixture) - actual = jsonutils.loads(actual) - for k in expected['date']: - self.assertEqual(expected['date'][k], actual['date'][k]) - - def test_to_json_with_more_deep_format(self): - fixture = {"is_public": True, "name": [{"name1": "test"}]} - expected = {"is_public": True, "name": [{"name1": "test"}]} - actual = rpc.RPCJSONSerializer().to_json(fixture) - actual = wsgi.JSONResponseSerializer().to_json(fixture) - actual = jsonutils.loads(actual) - for k in expected: - self.assertEqual(expected[k], actual[k]) - - def test_default(self): - fixture = {"key": "value"} - response = webob.Response() - rpc.RPCJSONSerializer().default(response, fixture) - self.assertEqual(http.OK, response.status_int) - content_types = [h for h in response.headerlist - if h[0] == 'Content-Type'] - self.assertEqual(1, len(content_types)) - self.assertEqual('application/json', response.content_type) - self.assertEqual(b'{"key": "value"}', response.body) - - -class TestRPCJSONDeserializer(test_utils.BaseTestCase): - - def test_has_body_no_content_length(self): - request = wsgi.Request.blank('/') - request.method = 'POST' - request.body = b'asdf' - request.headers.pop('Content-Length') - self.assertFalse(rpc.RPCJSONDeserializer().has_body(request)) - - def test_has_body_zero_content_length(self): - request = wsgi.Request.blank('/') - request.method = 'POST' - request.body = b'asdf' - request.headers['Content-Length'] = 0 - self.assertFalse(rpc.RPCJSONDeserializer().has_body(request)) - - def test_has_body_has_content_length(self): - request = wsgi.Request.blank('/') - request.method = 'POST' - request.body = b'asdf' - self.assertIn('Content-Length', request.headers) - self.assertTrue(rpc.RPCJSONDeserializer().has_body(request)) - - def test_no_body_no_content_length(self): - request = wsgi.Request.blank('/') - self.assertFalse(rpc.RPCJSONDeserializer().has_body(request)) - - def test_from_json(self): - fixture = '{"key": "value"}' - expected = {"key": "value"} - actual = rpc.RPCJSONDeserializer().from_json(fixture) - self.assertEqual(expected, actual) - - def test_from_json_malformed(self): - fixture = 'kjasdklfjsklajf' - self.assertRaises(webob.exc.HTTPBadRequest, - rpc.RPCJSONDeserializer().from_json, fixture) - - def test_default_no_body(self): - request = wsgi.Request.blank('/') - actual = rpc.RPCJSONDeserializer().default(request) - expected = {} - self.assertEqual(expected, actual) - - def test_default_with_body(self): - request = wsgi.Request.blank('/') - request.method = 'POST' - request.body = b'{"key": "value"}' - actual = rpc.RPCJSONDeserializer().default(request) - expected = {"body": {"key": "value"}} - self.assertEqual(expected, actual) - - def test_has_body_has_transfer_encoding(self): - request = wsgi.Request.blank('/') - request.method = 'POST' - request.body = b'fake_body' - request.headers['transfer-encoding'] = '' - self.assertIn('transfer-encoding', request.headers) - self.assertTrue(rpc.RPCJSONDeserializer().has_body(request)) - - def test_to_json_with_date_format_value(self): - fixture = ('{"date": {"_value": "1900-03-08T02:00:00.000000",' - '"_type": "datetime"}}') - expected = {"date": datetime.datetime(1900, 3, 8, 2)} - actual = rpc.RPCJSONDeserializer().from_json(fixture) - self.assertEqual(expected, actual) diff --git a/glance/tests/unit/test_cached_images.py b/glance/tests/unit/test_cached_images.py index 9bc8fcd4b0..4617bbdaaa 100644 --- a/glance/tests/unit/test_cached_images.py +++ b/glance/tests/unit/test_cached_images.py @@ -16,8 +16,8 @@ import testtools import webob -from glance.api import cached_images from glance.api import policy +from glance.api.v2 import cached_images from glance.common import exception from glance import image_cache @@ -71,7 +71,7 @@ class FakeCache(image_cache.ImageCache): return 1 -class FakeController(cached_images.Controller): +class FakeController(cached_images.CacheController): def __init__(self): self.cache = FakeCache() self.policy = FakePolicyEnforcer() @@ -80,7 +80,7 @@ class FakeController(cached_images.Controller): class TestController(testtools.TestCase): def test_initialization_without_conf(self): self.assertRaises(exception.BadDriverConfiguration, - cached_images.Controller) + cached_images.CacheController) class TestCachedImages(testtools.TestCase):