Support cinder multiple stores

This patch updates the location URL of the legacy images while
upgrading from single cinder store to multiple stores.
It does that with the help of lazy loading logic i.e. while
GET images call, it checks the location URL and metadata
of the image against the configured store ids and updates
images to respective stores on the basis of volume type (comparing
image-volume's type with the configured cinder_volume_type).
Legacy image URL:
cinder://<volume-id>
New image URL:
cinder://<store-id>/<volume-id>

NOTE: bumping lower-constraints/requirements of glance-store to 2.3.0 as
it includes changes[1] that are a hard requirement for cinder multiple
stores to work with glance

[1] https://review.opendev.org/#/c/746556/

Change-Id: I087a89c20813378fea8ff22ddf81d7a10c220db3
Implements: blueprint multiple-cinder-backend-support
This commit is contained in:
whoami-rajat 2020-08-25 21:28:45 +00:00
parent 5126ca0242
commit 98a1e792c6
8 changed files with 124 additions and 11 deletions

View File

@ -35,7 +35,7 @@ def lazy_update_store_info(func):
def wrapped(context, image, image_repo, **kwargs): def wrapped(context, image, image_repo, **kwargs):
if CONF.enabled_backends: if CONF.enabled_backends:
store_utils.update_store_in_locations( store_utils.update_store_in_locations(
image, image_repo) context, image, image_repo)
return func(context, image, image_repo, **kwargs) return func(context, image, image_repo, **kwargs)

View File

@ -21,7 +21,7 @@ from oslo_utils import encodeutils
import six.moves.urllib.parse as urlparse import six.moves.urllib.parse as urlparse
import glance.db as db_api import glance.db as db_api
from glance.i18n import _LE from glance.i18n import _LE, _LW
from glance import scrubber from glance import scrubber
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
@ -178,11 +178,15 @@ def _get_store_id_from_uri(uri):
return return
def update_store_in_locations(image, image_repo): def update_store_in_locations(context, image, image_repo):
store_updated = False
for loc in image.locations: for loc in image.locations:
if (not loc['metadata'].get( if (not loc['metadata'].get(
'store') or loc['metadata'].get( 'store') or loc['metadata'].get(
'store') not in CONF.enabled_backends): 'store') not in CONF.enabled_backends):
if loc['url'].startswith("cinder://"):
_update_cinder_location_and_store_id(context, loc)
store_id = _get_store_id_from_uri(loc['url']) store_id = _get_store_id_from_uri(loc['url'])
if store_id: if store_id:
if 'store' in loc['metadata']: if 'store' in loc['metadata']:
@ -195,10 +199,45 @@ def update_store_in_locations(image, image_repo):
'new': store_id, 'new': store_id,
'id': image.image_id}) 'id': image.image_id})
store_updated = True
loc['metadata']['store'] = store_id loc['metadata']['store'] = store_id
if store_updated:
image_repo.save(image) image_repo.save(image)
def _update_cinder_location_and_store_id(context, loc):
"""Update store location of legacy images
While upgrading from single cinder store to multiple stores,
the images having a store configured with a volume type matching
the image-volume's type will be migrated/associated to that store
and their location url will be updated respectively to the new format
i.e. cinder://store-id/volume-id
If there is no store configured for the image, the location url will
not be updated.
"""
uri = loc['url']
volume_id = loc['url'].split("/")[-1]
scheme = urlparse.urlparse(uri).scheme
location_map = store_api.location.SCHEME_TO_CLS_BACKEND_MAP
if scheme not in location_map:
LOG.warning(_LW("Unknown scheme '%(scheme)s' found in uri '%(uri)s'"),
{'scheme': scheme, 'uri': uri})
return
for store in location_map[scheme]:
store_instance = location_map[scheme][store]['store']
if store_instance.is_image_associated_with_store(context, volume_id):
url_prefix = store_instance.url_prefix
loc['url'] = "%s/%s" % (url_prefix, volume_id)
loc['metadata']['store'] = "%s" % store
return
LOG.warning(_LW("Not able to update location url '%s' of legacy image "
"due to unknown issues."), uri)
def get_updated_store_location(locations): def get_updated_store_location(locations):
for loc in locations: for loc in locations:
store_id = _get_store_id_from_uri(loc['url']) store_id = _get_store_id_from_uri(loc['url'])

View File

@ -71,7 +71,8 @@ class MultiStoreClearingUnitTest(test_utils.BaseTestCase):
:returns: the number of how many store drivers been loaded. :returns: the number of how many store drivers been loaded.
""" """
self.config(enabled_backends={'fast': 'file', 'cheap': 'file', self.config(enabled_backends={'fast': 'file', 'cheap': 'file',
'readonly_store': 'http'}) 'readonly_store': 'http',
'fast-cinder': 'cinder'})
store.register_store_opts(CONF) store.register_store_opts(CONF)
self.config(default_backend='fast', self.config(default_backend='fast',

View File

@ -14,10 +14,11 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
import glance_store as store
import tempfile import tempfile
from unittest import mock from unittest import mock
import glance_store as store
from glance_store._drivers import cinder
from oslo_config import cfg from oslo_config import cfg
from oslo_log import log as logging from oslo_log import log as logging
import six import six
@ -26,6 +27,7 @@ import webob
from glance.common import exception from glance.common import exception
from glance.common import store_utils from glance.common import store_utils
from glance.common import utils from glance.common import utils
from glance.tests.unit import base
from glance.tests import utils as test_utils from glance.tests import utils as test_utils
@ -41,6 +43,7 @@ class TestStoreUtils(test_utils.BaseTestCase):
image = mock.Mock() image = mock.Mock()
image_repo = mock.Mock() image_repo = mock.Mock()
image_repo.save = mock.Mock() image_repo.save = mock.Mock()
context = mock.Mock()
locations = [{ locations = [{
'url': 'rbd://aaaaaaaa/images/id', 'url': 'rbd://aaaaaaaa/images/id',
'metadata': metadata 'metadata': metadata
@ -49,7 +52,7 @@ class TestStoreUtils(test_utils.BaseTestCase):
with mock.patch.object( with mock.patch.object(
store_utils, '_get_store_id_from_uri') as mock_get_store_id: store_utils, '_get_store_id_from_uri') as mock_get_store_id:
mock_get_store_id.return_value = store_id mock_get_store_id.return_value = store_id
store_utils.update_store_in_locations(image, image_repo) store_utils.update_store_in_locations(context, image, image_repo)
self.assertEqual(image.locations[0]['metadata'].get( self.assertEqual(image.locations[0]['metadata'].get(
'store'), expected) 'store'), expected)
self.assertEqual(store_id_call_count, mock_get_store_id.call_count) self.assertEqual(store_id_call_count, mock_get_store_id.call_count)
@ -92,6 +95,64 @@ class TestStoreUtils(test_utils.BaseTestCase):
save_call_count=0) save_call_count=0)
class TestCinderStoreUtils(base.MultiStoreClearingUnitTest):
"""Test glance.common.store_utils module for cinder multistore"""
@mock.patch.object(cinder.Store, 'is_image_associated_with_store')
@mock.patch.object(cinder.Store, 'url_prefix',
new_callable=mock.PropertyMock)
def _test_update_cinder_store_in_location(self, mock_url_prefix,
mock_associate_store,
is_valid=True):
volume_id = 'db457a25-8f16-4b2c-a644-eae8d17fe224'
store_id = 'fast-cinder'
expected = 'fast-cinder'
image = mock.Mock()
image_repo = mock.Mock()
image_repo.save = mock.Mock()
context = mock.Mock()
mock_associate_store.return_value = is_valid
locations = [{
'url': 'cinder://%s' % volume_id,
'metadata': {}
}]
mock_url_prefix.return_value = 'cinder://%s' % store_id
image.locations = locations
store_utils.update_store_in_locations(context, image, image_repo)
if is_valid:
# This is the case where we found an image that has an
# old-style URL which does not include the store name,
# but for which we know the corresponding store that
# refers to the volume type that backs it. We expect that
# the URL should be updated to point to the store/volume from
# just a naked pointer to the volume, as was the old
# format i.e. this is the case when store is valid and location
# url, metadata are updated and image_repo.save is called
expected_url = mock_url_prefix.return_value + '/' + volume_id
self.assertEqual(expected_url, image.locations[0].get('url'))
self.assertEqual(expected, image.locations[0]['metadata'].get(
'store'))
self.assertEqual(1, image_repo.save.call_count)
else:
# Here, we've got an image backed by a volume which does
# not have a corresponding store specifying the volume_type.
# Expect that we leave these alone and do not touch the
# location URL since we cannot update it with a valid store i.e.
# this is the case when store is invalid and location url,
# metadata are not updated and image_repo.save is not called
self.assertEqual(locations[0]['url'],
image.locations[0].get('url'))
self.assertEqual({}, image.locations[0]['metadata'])
self.assertEqual(0, image_repo.save.call_count)
def test_update_cinder_store_location_valid_type(self):
self._test_update_cinder_store_in_location()
def test_update_cinder_store_location_invalid_type(self):
self._test_update_cinder_store_in_location(is_valid=False)
class TestUtils(test_utils.BaseTestCase): class TestUtils(test_utils.BaseTestCase):
"""Test routines in glance.utils""" """Test routines in glance.utils"""

View File

@ -39,7 +39,7 @@ class TestInfoControllers(base.MultiStoreClearingUnitTest):
req) req)
def test_get_stores(self): def test_get_stores(self):
available_stores = ['cheap', 'fast', 'readonly_store'] available_stores = ['cheap', 'fast', 'readonly_store', 'fast-cinder']
req = unit_test_utils.get_fake_request() req = unit_test_utils.get_fake_request()
output = self.controller.get_stores(req) output = self.controller.get_stores(req)
self.assertIn('stores', output) self.assertIn('stores', output)
@ -48,7 +48,7 @@ class TestInfoControllers(base.MultiStoreClearingUnitTest):
self.assertIn(stores['id'], available_stores) self.assertIn(stores['id'], available_stores)
def test_get_stores_read_only_store(self): def test_get_stores_read_only_store(self):
available_stores = ['cheap', 'fast', 'readonly_store'] available_stores = ['cheap', 'fast', 'readonly_store', 'fast-cinder']
req = unit_test_utils.get_fake_request() req = unit_test_utils.get_fake_request()
output = self.controller.get_stores(req) output = self.controller.get_stores(req)
self.assertIn('stores', output) self.assertIn('stores', output)

View File

@ -29,7 +29,7 @@ fasteners==0.14.1
fixtures==3.0.0 fixtures==3.0.0
future==0.16.0 future==0.16.0
futurist==1.2.0 futurist==1.2.0
glance-store==1.0.0 glance-store==2.3.0
greenlet==0.4.13 greenlet==0.4.13
httplib2==0.9.1 httplib2==0.9.1
idna==2.6 idna==2.6

View File

@ -0,0 +1,12 @@
---
features:
- |
Added support for cinder multiple stores.
upgrade:
- |
During upgrade from single cinder store to multiple cinder stores, legacy
images location url will be updated to the new format with respect to the
volume type configured in the stores.
Legacy location url: cinder://<volume-id>
New location url: cinder://<store-id>/<volume-id>

View File

@ -48,7 +48,7 @@ retrying!=1.3.0,>=1.2.3 # Apache-2.0
osprofiler>=1.4.0 # Apache-2.0 osprofiler>=1.4.0 # Apache-2.0
# Glance Store # Glance Store
glance-store>=1.0.0 # Apache-2.0 glance-store>=2.3.0 # Apache-2.0
debtcollector>=1.2.0 # Apache-2.0 debtcollector>=1.2.0 # Apache-2.0