diff --git a/glance/api/v2/discovery.py b/glance/api/v2/discovery.py index 4efd8e60e8..9577c3d7eb 100644 --- a/glance/api/v2/discovery.py +++ b/glance/api/v2/discovery.py @@ -14,8 +14,10 @@ # limitations under the License. from oslo_config import cfg +import webob.exc from glance.common import wsgi +from glance.i18n import _ CONF = cfg.CONF @@ -34,6 +36,27 @@ class InfoController(object): 'import-methods': import_methods } + def get_stores(self, req): + # TODO(abhishekk): This will be removed after config options + # 'stores' and 'default_store' are removed. + enabled_backends = CONF.enabled_backends + if not enabled_backends: + msg = _("Multi backend is not supported at this site.") + raise webob.exc.HTTPNotFound(explanation=msg) + + backends = [] + for backend in enabled_backends: + stores = {} + stores['id'] = backend + description = getattr(CONF, backend).store_description + if description: + stores['description'] = description + if backend == CONF.glance_store.default_backend: + stores['default'] = "true" + backends.append(stores) + + return {'stores': backends} + def create_resource(): return wsgi.Resource(InfoController()) diff --git a/glance/api/v2/image_data.py b/glance/api/v2/image_data.py index 33d9b11d82..b67604ceee 100644 --- a/glance/api/v2/image_data.py +++ b/glance/api/v2/image_data.py @@ -100,6 +100,18 @@ class ImageDataController(object): @utils.mutating def upload(self, req, image_id, data, size): + backend = None + if CONF.enabled_backends: + backend = req.headers.get('x-image-meta-store', + CONF.glance_store.default_backend) + + try: + glance_store.get_store_from_store_identifier(backend) + except glance_store.UnknownScheme as exc: + raise webob.exc.HTTPBadRequest(explanation=exc.msg, + request=req, + content_type='text/plain') + image_repo = self.gateway.get_repo(req.context) image = None refresher = None @@ -129,7 +141,7 @@ class ImageDataController(object): encodeutils.exception_to_unicode(e)) image_repo.save(image, from_state='queued') - image.set_data(data, size) + image.set_data(data, size, backend=backend) try: image_repo.save(image, from_state='saving') @@ -274,9 +286,16 @@ class ImageDataController(object): # NOTE(jokke): this is horrible way to do it but as long as # glance_store is in a shape it is, the only way. Don't hold me # accountable for it. + # TODO(abhishekk): After removal of backend module from glance_store + # need to change this to use multi_backend module. def _build_staging_store(): conf = cfg.ConfigOpts() - backend.register_opts(conf) + + try: + backend.register_opts(conf) + except cfg.DuplicateOptError: + pass + conf.set_override('filesystem_store_datadir', CONF.node_staging_uri[7:], group='glance_store') diff --git a/glance/api/v2/images.py b/glance/api/v2/images.py index 9d70a24d39..f551bb1c58 100644 --- a/glance/api/v2/images.py +++ b/glance/api/v2/images.py @@ -94,10 +94,6 @@ class ImagesController(object): task_factory = self.gateway.get_task_factory(req.context) executor_factory = self.gateway.get_task_executor_factory(req.context) task_repo = self.gateway.get_task_repo(req.context) - - task_input = {'image_id': image_id, - 'import_req': body} - import_method = body.get('method').get('name') uri = body.get('method').get('uri') @@ -121,11 +117,26 @@ class ImagesController(object): if not getattr(image, 'disk_format', None): msg = _("'disk_format' needs to be set before import") raise exception.Conflict(msg) + + backend = None + if CONF.enabled_backends: + backend = req.headers.get('x-image-meta-store', + CONF.glance_store.default_backend) + try: + glance_store.get_store_from_store_identifier(backend) + except glance_store.UnknownScheme: + msg = _("Store for scheme %s not found") % backend + LOG.warn(msg) + raise exception.Conflict(msg) except exception.Conflict as e: raise webob.exc.HTTPConflict(explanation=e.msg) except exception.NotFound as e: raise webob.exc.HTTPNotFound(explanation=e.msg) + task_input = {'image_id': image_id, + 'import_req': body, + 'backend': backend} + if (import_method == 'web-download' and not utils.validate_import_uri(uri)): LOG.debug("URI for web-download does not pass filtering: %s", @@ -324,7 +335,10 @@ class ImagesController(object): if image.status == 'uploading': file_path = str(CONF.node_staging_uri + '/' + image.image_id) - self.store_api.delete_from_backend(file_path) + if CONF.enabled_backends: + self.store_api.delete(file_path, None) + else: + self.store_api.delete_from_backend(file_path) image.delete() image_repo.remove(image) @@ -926,6 +940,20 @@ class ResponseSerializer(wsgi.JSONResponseSerializer): image_view['file'] = self._get_image_href(image, 'file') image_view['schema'] = '/v2/schemas/image' image_view = self.schema.filter(image_view) # domain + + # add store information to image + if CONF.enabled_backends: + locations = _get_image_locations(image) + if locations: + stores = [] + for loc in locations: + backend = loc['metadata'].get('backend') + if backend: + stores.append(backend) + + if stores: + image_view['stores'] = ",".join(stores) + return image_view except exception.Forbidden as e: raise webob.exc.HTTPForbidden(explanation=e.msg) @@ -941,6 +969,11 @@ class ResponseSerializer(wsgi.JSONResponseSerializer): ','.join(CONF.enabled_import_methods)) response.headerlist.append(import_methods) + if CONF.enabled_backends: + enabled_backends = ("OpenStack-image-store-ids", + ','.join(CONF.enabled_backends.keys())) + response.headerlist.append(enabled_backends) + def show(self, response, image): image_view = self._format_image(image) body = json.dumps(image_view, ensure_ascii=False) @@ -1107,6 +1140,11 @@ def get_base_properties(): 'readOnly': True, 'description': _('An image file url'), }, + 'backend': { + 'type': 'string', + 'readOnly': True, + 'description': _('Backend store to upload image to'), + }, 'schema': { 'type': 'string', 'readOnly': True, diff --git a/glance/api/v2/router.py b/glance/api/v2/router.py index 9b45cd457a..0980e11922 100644 --- a/glance/api/v2/router.py +++ b/glance/api/v2/router.py @@ -565,5 +565,14 @@ class API(wsgi.Router): controller=reject_method_resource, action='reject', allowed_methods='GET') + mapper.connect('/info/stores', + controller=info_resource, + action='get_stores', + conditions={'method': ['GET']}, + body_reject=True) + mapper.connect('/info/stores', + controller=reject_method_resource, + action='reject', + allowed_methods='GET') super(API, self).__init__(mapper) diff --git a/glance/async/flows/_internal_plugins/web_download.py b/glance/async/flows/_internal_plugins/web_download.py index 04cb33e463..a58e5d0293 100644 --- a/glance/async/flows/_internal_plugins/web_download.py +++ b/glance/async/flows/_internal_plugins/web_download.py @@ -61,8 +61,14 @@ class _WebDownload(task.Task): # glance_store refactor is done. A good thing is that glance_store is # under our team's management and it gates on Glance so changes to # this API will (should?) break task's tests. + # TODO(abhishekk): After removal of backend module from glance_store + # need to change this to use multi_backend module. conf = cfg.ConfigOpts() - backend.register_opts(conf) + try: + backend.register_opts(conf) + except cfg.DuplicateOptError: + pass + conf.set_override('filesystem_store_datadir', CONF.node_staging_uri[7:], group='glance_store') diff --git a/glance/async/flows/api_image_import.py b/glance/async/flows/api_image_import.py index 3ca10aa2db..dc08d371ff 100644 --- a/glance/async/flows/api_image_import.py +++ b/glance/async/flows/api_image_import.py @@ -86,7 +86,10 @@ class _DeleteFromFS(task.Task): :param file_path: path to the file being deleted """ - store_api.delete_from_backend(file_path) + if CONF.enabled_backends: + store_api.delete(file_path, None) + else: + store_api.delete_from_backend(file_path) class _VerifyStaging(task.Task): @@ -122,6 +125,8 @@ class _VerifyStaging(task.Task): self._build_store() def _build_store(self): + # TODO(abhishekk): After removal of backend module from glance_store + # need to change this to use multi_backend module. # NOTE(jokke): If we want to use some other store for staging, we can # implement the logic more general here. For now this should do. # NOTE(flaper87): Due to the nice glance_store api (#sarcasm), we're @@ -133,7 +138,10 @@ class _VerifyStaging(task.Task): # under our team's management and it gates on Glance so changes to # this API will (should?) break task's tests. conf = cfg.ConfigOpts() - backend.register_opts(conf) + try: + backend.register_opts(conf) + except cfg.DuplicateOptError: + pass conf.set_override('filesystem_store_datadir', CONF.node_staging_uri[7:], group='glance_store') @@ -159,12 +167,13 @@ class _VerifyStaging(task.Task): class _ImportToStore(task.Task): - def __init__(self, task_id, task_type, image_repo, uri, image_id): + def __init__(self, task_id, task_type, image_repo, uri, image_id, backend): self.task_id = task_id self.task_type = task_type self.image_repo = image_repo self.uri = uri self.image_id = image_id + self.backend = backend super(_ImportToStore, self).__init__( name='%s-ImportToStore-%s' % (task_type, task_id)) @@ -215,7 +224,8 @@ class _ImportToStore(task.Task): # will need the file path anyways for our delete workflow for now. # For future proofing keeping this as is. image = self.image_repo.get(self.image_id) - image_import.set_image_data(image, file_path or self.uri, self.task_id) + image_import.set_image_data(image, file_path or self.uri, self.task_id, + backend=self.backend) # NOTE(flaper87): We need to save the image again after the locations # have been set in the image. @@ -306,6 +316,7 @@ def get_flow(**kwargs): image_id = kwargs.get('image_id') import_method = kwargs.get('import_req')['method']['name'] uri = kwargs.get('import_req')['method'].get('uri') + backend = kwargs.get('backend') separator = '' if not CONF.node_staging_uri.endswith('/'): @@ -332,7 +343,8 @@ def get_flow(**kwargs): task_type, image_repo, file_uri, - image_id) + image_id, + backend) flow.add(import_to_store) delete_task = lf.Flow(task_type).add(_DeleteFromFS(task_id, task_type)) diff --git a/glance/async/taskflow_executor.py b/glance/async/taskflow_executor.py index 091f92af67..3fe810da9d 100644 --- a/glance/async/taskflow_executor.py +++ b/glance/async/taskflow_executor.py @@ -129,6 +129,7 @@ class TaskExecutor(glance.async.TaskExecutor): if task.type == 'api_image_import': kwds['image_id'] = task_input['image_id'] kwds['import_req'] = task_input['import_req'] + kwds['backend'] = task_input['backend'] return driver.DriverManager('glance.flows', task.type, invoke_on_load=True, invoke_kwds=kwds).driver diff --git a/glance/common/scripts/image_import/main.py b/glance/common/scripts/image_import/main.py index 8906a0c471..9900a595ad 100644 --- a/glance/common/scripts/image_import/main.py +++ b/glance/common/scripts/image_import/main.py @@ -137,13 +137,13 @@ def create_image(image_repo, image_factory, image_properties, task_id): return image -def set_image_data(image, uri, task_id): +def set_image_data(image, uri, task_id, backend=None): data_iter = None try: LOG.info(_LI("Task %(task_id)s: Got image data uri %(data_uri)s to be " "imported"), {"data_uri": uri, "task_id": task_id}) data_iter = script_utils.get_image_data_iter(uri) - image.set_data(data_iter) + image.set_data(data_iter, backend=backend) except Exception as e: with excutils.save_and_reraise_exception(): LOG.warn(_LW("Task %(task_id)s failed with exception %(error)s") % diff --git a/glance/common/store_utils.py b/glance/common/store_utils.py index 0593f11df4..9ff3d19b01 100644 --- a/glance/common/store_utils.py +++ b/glance/common/store_utils.py @@ -46,7 +46,15 @@ def safe_delete_from_backend(context, image_id, location): """ try: - ret = store_api.delete_from_backend(location['url'], context=context) + if CONF.enabled_backends: + backend = location['metadata'].get('backend') + ret = store_api.delete(location['url'], + backend, + context=context) + else: + ret = store_api.delete_from_backend(location['url'], + context=context) + location['status'] = 'deleted' if 'id' in location: db_api.get_api().image_location_delete(context, image_id, @@ -133,5 +141,9 @@ def validate_external_location(uri): # TODO(zhiyan): This function could be moved to glance_store. # TODO(gm): Use a whitelist of allowed schemes scheme = urlparse.urlparse(uri).scheme - return (scheme in store_api.get_known_schemes() and + known_schemes = store_api.get_known_schemes() + if CONF.enabled_backends: + known_schemes = store_api.get_known_schemes_for_multi_store() + + return (scheme in known_schemes and scheme not in RESTRICTED_URI_SCHEMAS) diff --git a/glance/common/wsgi.py b/glance/common/wsgi.py index 12ea42e3b8..bc0bb084ff 100644 --- a/glance/common/wsgi.py +++ b/glance/common/wsgi.py @@ -317,6 +317,13 @@ wsgi_opts = [ '"HTTP_X_FORWARDED_PROTO".')), ] +store_opts = [ + cfg.DictOpt('enabled_backends', + help=_('Key:Value pair of store identifier and store type. ' + 'In case of multiple backends should be separated' + 'using comma.')), +] + LOG = logging.getLogger(__name__) @@ -325,6 +332,7 @@ CONF.register_opts(bind_opts) CONF.register_opts(socket_opts) CONF.register_opts(eventlet_opts) CONF.register_opts(wsgi_opts) +CONF.register_opts(store_opts) profiler_opts.set_defaults(CONF) ASYNC_EVENTLET_THREAD_POOL_LIST = [] @@ -448,6 +456,13 @@ def initialize_glance_store(): glance_store.verify_default_store() +def initialize_multi_store(): + """Initialize glance multi store backends.""" + glance_store.register_store_opts(CONF) + glance_store.create_multi_stores(CONF) + glance_store.verify_store() + + def get_asynchronous_eventlet_pool(size=1000): """Return eventlet pool to caller. @@ -599,7 +614,10 @@ class Server(object): self.client_socket_timeout = CONF.client_socket_timeout or None self.configure_socket(old_conf, has_changed) if self.initialize_glance_store: - initialize_glance_store() + if CONF.enabled_backends: + initialize_multi_store() + else: + initialize_glance_store() def reload(self): """ diff --git a/glance/common/wsgi_app.py b/glance/common/wsgi_app.py index 3c7c6d67ae..f4675746c4 100644 --- a/glance/common/wsgi_app.py +++ b/glance/common/wsgi_app.py @@ -22,6 +22,7 @@ from glance import notifier CONF = cfg.CONF CONF.import_group("profiler", "glance.common.wsgi") +CONF.import_opt("enabled_backends", "glance.common.wsgi") logging.register_options(CONF) CONFIG_FILES = ['glance-api-paste.ini', @@ -60,8 +61,15 @@ def init_app(): config_files = _get_config_files() CONF([], project='glance', default_config_files=config_files) logging.setup(CONF, "glance") - glance_store.register_opts(CONF) - glance_store.create_stores(CONF) - glance_store.verify_default_store() + + if CONF.enabled_backends: + glance_store.register_store_opts(CONF) + glance_store.create_multi_stores(CONF) + glance_store.verify_store() + else: + glance_store.register_opts(CONF) + glance_store.create_stores(CONF) + glance_store.verify_default_store() + _setup_os_profiler() return config.load_paste_app('glance-api') diff --git a/glance/domain/__init__.py b/glance/domain/__init__.py index 6d78c3dd9e..2d5b7af0e9 100644 --- a/glance/domain/__init__.py +++ b/glance/domain/__init__.py @@ -283,7 +283,7 @@ class Image(object): def get_data(self, *args, **kwargs): raise NotImplementedError() - def set_data(self, data, size=None): + def set_data(self, data, size=None, backend=None): raise NotImplementedError() diff --git a/glance/domain/proxy.py b/glance/domain/proxy.py index 7bfd458126..60c54a9246 100644 --- a/glance/domain/proxy.py +++ b/glance/domain/proxy.py @@ -194,8 +194,8 @@ class Image(object): def reactivate(self): self.base.reactivate() - def set_data(self, data, size=None): - self.base.set_data(data, size) + def set_data(self, data, size=None, backend=None): + self.base.set_data(data, size, backend=backend) def get_data(self, *args, **kwargs): return self.base.get_data(*args, **kwargs) diff --git a/glance/location.py b/glance/location.py index 345dc62734..82f146baa2 100644 --- a/glance/location.py +++ b/glance/location.py @@ -59,9 +59,16 @@ class ImageRepoProxy(glance.domain.proxy.Repo): self.store_api) member_ids = [m.member_id for m in member_repo.list()] for location in image.locations: - self.store_api.set_acls(location['url'], public=public, - read_tenants=member_ids, - context=self.context) + if CONF.enabled_backends: + self.store_api.set_acls_for_multi_store( + location['url'], location['metadata']['backend'], + public=public, read_tenants=member_ids, + context=self.context + ) + else: + self.store_api.set_acls(location['url'], public=public, + read_tenants=member_ids, + context=self.context) def add(self, image): result = super(ImageRepoProxy, self).add(image) @@ -82,19 +89,28 @@ def _get_member_repo_for_store(image, context, db_api, store_api): return store_image_repo -def _check_location_uri(context, store_api, store_utils, uri): +def _check_location_uri(context, store_api, store_utils, uri, + backend=None): """Check if an image location is valid. :param context: Glance request context :param store_api: store API module :param store_utils: store utils module :param uri: location's uri string + :param backend: A backend name for the store """ try: # NOTE(zhiyan): Some stores return zero when it catch exception + if CONF.enabled_backends: + size_from_backend = store_api.get_size_from_uri_and_backend( + uri, backend, context=context) + else: + size_from_backend = store_api.get_size_from_backend( + uri, context=context) + is_ok = (store_utils.validate_external_location(uri) and - store_api.get_size_from_backend(uri, context=context) > 0) + size_from_backend > 0) except (store.UnknownScheme, store.NotFound, store.BadStoreUri): is_ok = False if not is_ok: @@ -103,15 +119,25 @@ def _check_location_uri(context, store_api, store_utils, uri): def _check_image_location(context, store_api, store_utils, location): - _check_location_uri(context, store_api, store_utils, location['url']) + backend = None + if CONF.enabled_backends: + backend = location['metadata'].get('backend') + + _check_location_uri(context, store_api, store_utils, location['url'], + backend=backend) store_api.check_location_metadata(location['metadata']) def _set_image_size(context, image, locations): if not image.size: for location in locations: - size_from_backend = store.get_size_from_backend( - location['url'], context=context) + if CONF.enabled_backends: + size_from_backend = store.get_size_from_uri_and_backend( + location['url'], location['metadata'].get('backend'), + context=context) + else: + size_from_backend = store.get_size_from_backend( + location['url'], context=context) if size_from_backend: # NOTE(flwang): This assumes all locations have the same size @@ -404,7 +430,7 @@ class ImageProxy(glance.domain.proxy.Image): self.image.image_id, location) - def set_data(self, data, size=None): + def set_data(self, data, size=None, backend=None): if size is None: size = 0 # NOTE(markwash): zero -> unknown size @@ -429,20 +455,32 @@ class ImageProxy(glance.domain.proxy.Image): verifier = None hashing_algo = CONF['hashing_algorithm'] - - (location, - size, - checksum, - multihash, - loc_meta) = self.store_api.add_to_backend_with_multihash( - CONF, - self.image.image_id, - utils.LimitingReader(utils.CooperativeReader(data), - CONF.image_size_cap), - size, - hashing_algo, - context=self.context, - verifier=verifier) + if CONF.enabled_backends: + (location, size, checksum, + multihash, loc_meta) = self.store_api.add_with_multihash( + CONF, + self.image.image_id, + utils.LimitingReader(utils.CooperativeReader(data), + CONF.image_size_cap), + size, + backend, + hashing_algo, + context=self.context, + verifier=verifier) + else: + (location, + size, + checksum, + multihash, + loc_meta) = self.store_api.add_to_backend_with_multihash( + CONF, + self.image.image_id, + utils.LimitingReader(utils.CooperativeReader(data), + CONF.image_size_cap), + size, + hashing_algo, + context=self.context, + verifier=verifier) # NOTE(bpoulos): if verification fails, exception will be raised if verifier: @@ -451,8 +489,12 @@ class ImageProxy(glance.domain.proxy.Image): LOG.info(_LI("Successfully verified signature for image %s"), self.image.image_id) except crypto_exception.InvalidSignature: - self.store_api.delete_from_backend(location, - context=self.context) + if CONF.enabled_backends: + self.store_api.delete(location, loc_meta.get('backend'), + context=self.context) + else: + self.store_api.delete_from_backend(location, + context=self.context) raise cursive_exception.SignatureVerificationError( _('Signature verification failed') ) @@ -476,11 +518,18 @@ class ImageProxy(glance.domain.proxy.Image): err = None for loc in self.image.locations: try: - data, size = self.store_api.get_from_backend( - loc['url'], - offset=offset, - chunk_size=chunk_size, - context=self.context) + backend = loc['metadata'].get('backend') + if CONF.enabled_backends: + data, size = self.store_api.get( + loc['url'], backend, offset=offset, + chunk_size=chunk_size, context=self.context + ) + else: + data, size = self.store_api.get_from_backend( + loc['url'], + offset=offset, + chunk_size=chunk_size, + context=self.context) return data except Exception as e: @@ -490,8 +539,9 @@ class ImageProxy(glance.domain.proxy.Image): 'err': encodeutils.exception_to_unicode(e)}) err = e # tried all locations - LOG.error(_LE('Glance tried all active locations to get data for ' - 'image %s but all have failed.') % self.image.image_id) + LOG.error(_LE( + 'Glance tried all active locations/stores to get data ' + 'for image %s but all have failed.') % self.image.image_id) raise err diff --git a/glance/notifier.py b/glance/notifier.py index d2c2075bbe..8db5efc461 100644 --- a/glance/notifier.py +++ b/glance/notifier.py @@ -315,11 +315,16 @@ class NotificationBase(object): def get_payload(self, obj): return {} - def send_notification(self, notification_id, obj, extra_payload=None): + def send_notification(self, notification_id, obj, extra_payload=None, + backend=None): payload = self.get_payload(obj) if extra_payload is not None: payload.update(extra_payload) + # update backend information in the notification + if backend: + payload["backend"] = backend + _send_notification(self.notifier.info, notification_id, payload) @@ -419,12 +424,12 @@ class ImageProxy(NotificationProxy, domain_proxy.Image): data = self.repo.get_data(offset=offset, chunk_size=chunk_size) return self._get_chunk_data_iterator(data, chunk_size=chunk_size) - def set_data(self, data, size=None): - self.send_notification('image.prepare', self.repo) + def set_data(self, data, size=None, backend=None): + self.send_notification('image.prepare', self.repo, backend=backend) notify_error = self.notifier.error try: - self.repo.set_data(data, size) + self.repo.set_data(data, size, backend=backend) except glance_store.StorageFull as e: msg = (_("Image storage media is full: %s") % encodeutils.exception_to_unicode(e)) diff --git a/glance/quota/__init__.py b/glance/quota/__init__.py index 0e2ce4fa7e..1d721c5012 100644 --- a/glance/quota/__init__.py +++ b/glance/quota/__init__.py @@ -57,8 +57,13 @@ def _calc_required_size(context, image, locations): size_from_backend = None try: - size_from_backend = store.get_size_from_backend( - location['url'], context=context) + if CONF.enabled_backends: + size_from_backend = store.get_size_from_uri_and_backend( + location['url'], location['metadata'].get('backend'), + context=context) + else: + size_from_backend = store.get_size_from_backend( + location['url'], context=context) except (store.UnknownScheme, store.NotFound): pass except store.BadStoreUri: @@ -293,7 +298,7 @@ class ImageProxy(glance.domain.proxy.Image): super(ImageProxy, self).__init__(image) self.orig_props = set(image.extra_properties.keys()) - def set_data(self, data, size=None): + def set_data(self, data, size=None, backend=None): remaining = glance.api.common.check_quota( self.context, size, self.db_api, image_id=self.image.image_id) if remaining is not None: @@ -302,7 +307,7 @@ class ImageProxy(glance.domain.proxy.Image): data = utils.LimitingReader( data, remaining, exception_class=exception.StorageQuotaFull) - self.image.set_data(data, size=size) + self.image.set_data(data, size=size, backend=backend) # NOTE(jbresnah) If two uploads happen at the same time and neither # properly sets the size attribute[1] then there is a race condition diff --git a/glance/scrubber.py b/glance/scrubber.py index 28cb912956..a38aa06b8a 100644 --- a/glance/scrubber.py +++ b/glance/scrubber.py @@ -253,7 +253,13 @@ class ScrubDBQueue(object): else: uri = loc['url'] - ret.append((image['id'], loc['id'], uri)) + # if multi-store is enabled then we need to pass backend + # to delete the image. + backend = loc['metadata'].get('backend') + if CONF.enabled_backends: + ret.append((image['id'], loc['id'], uri, backend)) + else: + ret.append((image['id'], loc['id'], uri)) return ret def has_image(self, image_id): @@ -327,10 +333,18 @@ class Scrubber(object): raise exception.FailedToGetScrubberJobs() delete_jobs = {} - for image_id, loc_id, loc_uri in records: - if image_id not in delete_jobs: - delete_jobs[image_id] = [] - delete_jobs[image_id].append((image_id, loc_id, loc_uri)) + if CONF.enabled_backends: + for image_id, loc_id, loc_uri, backend in records: + if image_id not in delete_jobs: + delete_jobs[image_id] = [] + delete_jobs[image_id].append((image_id, loc_id, + loc_uri, backend)) + else: + for image_id, loc_id, loc_uri in records: + if image_id not in delete_jobs: + delete_jobs[image_id] = [] + delete_jobs[image_id].append((image_id, loc_id, loc_uri)) + return delete_jobs def run(self, event=None): @@ -347,11 +361,21 @@ class Scrubber(object): {'id': image_id, 'count': len(delete_jobs)}) success = True - for img_id, loc_id, uri in delete_jobs: - try: - self._delete_image_location_from_backend(img_id, loc_id, uri) - except Exception: - success = False + if CONF.enabled_backends: + for img_id, loc_id, uri, backend in delete_jobs: + try: + self._delete_image_location_from_backend(img_id, loc_id, + uri, + backend=backend) + except Exception: + success = False + else: + for img_id, loc_id, uri in delete_jobs: + try: + self._delete_image_location_from_backend(img_id, loc_id, + uri) + except Exception: + success = False if success: image = db_api.get_api().image_get(self.admin_context, image_id) @@ -364,11 +388,15 @@ class Scrubber(object): "from backend. Leaving image '%s' in 'pending_delete'" " status") % image_id) - def _delete_image_location_from_backend(self, image_id, loc_id, uri): + def _delete_image_location_from_backend(self, image_id, loc_id, uri, + backend=None): try: LOG.debug("Scrubbing image %s from a location.", image_id) try: - self.store_api.delete_from_backend(uri, self.admin_context) + if CONF.enabled_backends: + self.store_api.delete(uri, backend, self.admin_context) + else: + self.store_api.delete_from_backend(uri, self.admin_context) except store_exceptions.NotFound: LOG.info(_LI("Image location for image '%s' not found in " "backend; Marking image location deleted in " diff --git a/glance/tests/functional/v2/test_schemas.py b/glance/tests/functional/v2/test_schemas.py index 9e93b44c07..4ccc89e5a1 100644 --- a/glance/tests/functional/v2/test_schemas.py +++ b/glance/tests/functional/v2/test_schemas.py @@ -58,6 +58,7 @@ class TestSchemas(functional.FunctionalTest): 'min_disk', 'protected', 'os_hidden', + 'backend' ]) self.assertEqual(expected, set(image_schema['properties'].keys())) diff --git a/glance/tests/unit/test_notifier.py b/glance/tests/unit/test_notifier.py index 9219ad2069..04f56f3675 100644 --- a/glance/tests/unit/test_notifier.py +++ b/glance/tests/unit/test_notifier.py @@ -44,7 +44,7 @@ class ImageStub(glance.domain.Image): def get_data(self, offset=0, chunk_size=None): return ['01234', '56789'] - def set_data(self, data, size=None): + def set_data(self, data, size, backend=None): for chunk in data: pass diff --git a/glance/tests/unit/test_quota.py b/glance/tests/unit/test_quota.py index d2f6658328..6419adc6c6 100644 --- a/glance/tests/unit/test_quota.py +++ b/glance/tests/unit/test_quota.py @@ -42,7 +42,7 @@ class FakeImage(object): locations = [{'url': 'file:///not/a/path', 'metadata': {}}] tags = set([]) - def set_data(self, data, size=None): + def set_data(self, data, size=None, backend=None): self.size = 0 for d in data: self.size += len(d) diff --git a/glance/tests/unit/v2/test_image_data_resource.py b/glance/tests/unit/v2/test_image_data_resource.py index c301dc2666..44ae67ea56 100644 --- a/glance/tests/unit/v2/test_image_data_resource.py +++ b/glance/tests/unit/v2/test_image_data_resource.py @@ -69,7 +69,7 @@ class FakeImage(object): return self.data[offset:offset + chunk_size] return self.data[offset:] - def set_data(self, data, size=None): + def set_data(self, data, size=None, backend=None): self.data = ''.join(data) self.size = size self.status = 'modified-by-fake' diff --git a/glance/tests/unit/v2/test_schemas_resource.py b/glance/tests/unit/v2/test_schemas_resource.py index adcc9361e0..887cdb0824 100644 --- a/glance/tests/unit/v2/test_schemas_resource.py +++ b/glance/tests/unit/v2/test_schemas_resource.py @@ -34,7 +34,7 @@ class TestSchemasController(test_utils.BaseTestCase): 'file', 'container_format', 'schema', 'id', 'size', 'direct_url', 'min_ram', 'min_disk', 'protected', 'locations', 'owner', 'virtual_size', 'os_hidden', - 'os_hash_algo', 'os_hash_value']) + 'os_hash_algo', 'os_hash_value', 'backend']) self.assertEqual(expected, set(output['properties'].keys())) def test_image_has_correct_statuses(self):