Merge "Support Cinder multiple stores"

This commit is contained in:
Zuul 2020-08-25 15:06:30 +00:00 committed by Gerrit Code Review
commit 78217017e2
4 changed files with 197 additions and 64 deletions

View File

@ -360,13 +360,16 @@ class StoreLocation(glance_store.location.StoreLocation):
self.volume_id = self.specs.get('volume_id')
def get_uri(self):
if self.backend_group:
return "cinder://%s/%s" % (self.backend_group,
self.volume_id)
return "cinder://%s" % self.volume_id
def parse_uri(self, uri):
self.validate_schemas(uri, valid_schemas=('cinder://',))
self.scheme = 'cinder'
self.volume_id = uri[9:]
self.volume_id = uri.split('/')[-1]
if not utils.is_uuid_like(self.volume_id):
reason = _("URI contains invalid volume ID")
@ -386,57 +389,126 @@ class Store(glance_store.driver.Store):
def __init__(self, *args, **kargs):
super(Store, self).__init__(*args, **kargs)
if self.backend_group:
self._set_url_prefix()
# We are importing it here to let the config options load
# before we use them in the fs_mount file
self.mount = importlib.import_module('glance_store.common.fs_mount')
self._set_url_prefix()
if self.backend_group:
self.store_conf = getattr(self.conf, self.backend_group)
else:
self.store_conf = self.conf.glance_store
def _set_url_prefix(self):
self._url_prefix = "cinder://"
if self.backend_group:
self._url_prefix = "cinder://%s" % self.backend_group
def configure_add(self):
"""
Configure the Store to use the stored configuration options
Any store that needs special configuration should implement
this method. If the store was not able to successfully configure
itself, it should raise `exceptions.BadStoreConfiguration`
:raises: `exceptions.BadStoreConfiguration` if multiple stores are
defined and particular store wasn't able to configure
successfully
:raises: `exceptions.BackendException` if single store is defined and
it wasn't able to configure successfully
"""
if self.backend_group:
cinder_volume_type = self.store_conf.cinder_volume_type
if cinder_volume_type:
# NOTE: `cinder_volume_type` is configured, check
# configured volume_type is available in cinder or not
cinder_client = self.get_cinderclient()
try:
# We don't even need the volume type object, as long
# as this returns clean, we know the name is good.
cinder_client.volume_types.find(name=cinder_volume_type)
# No need to worry NoUniqueMatch as volume type name is
# unique
except cinder_exception.NotFound:
reason = _("Invalid `cinder_volume_type %s`"
% cinder_volume_type)
if len(self.conf.enabled_backends) > 1:
LOG.error(reason)
raise exceptions.BadStoreConfiguration(
store_name=self.backend_group, reason=reason)
else:
LOG.critical(reason)
raise exceptions.BackendException(reason)
def is_image_associated_with_store(self, context, volume_id):
"""
Updates legacy images URL to respective stores.
This method checks the volume type of the volume associated with the
image against the configured stores. It returns true if the
cinder_volume_type configured in the store matches with the volume
type of the image-volume. When cinder_volume_type is not configured
then the it checks it against default_volume_type set in cinder.
If above both conditions doesn't meet, it returns false.
"""
try:
cinder_client = self.get_cinderclient(context=context,
legacy_update=True)
cinder_volume_type = self.store_conf.cinder_volume_type
volume = cinder_client.volumes.get(volume_id)
if cinder_volume_type and volume.volume_type == cinder_volume_type:
return True
elif not cinder_volume_type:
default_type = cinder_client.volume_types.default()['name']
if volume.volume_type == default_type:
return True
except Exception:
# Glance calls this method to update legacy images URL
# If an exception occours due to image/volume is non-existent or
# any other reason, we return False (i.e. the image location URL
# won't be updated) and it is glance's responsibility to handle
# the case when the image failed to update
pass
return False
def get_root_helper(self):
if self.backend_group:
rootwrap = getattr(CONF, self.backend_group).rootwrap_config
else:
rootwrap = CONF.glance_store.rootwrap_config
rootwrap = self.store_conf.rootwrap_config
return 'sudo glance-rootwrap %s' % rootwrap
def is_user_overriden(self):
if self.backend_group:
store_conf = getattr(self.conf, self.backend_group)
else:
store_conf = self.conf.glance_store
return all([store_conf.get('cinder_store_' + key)
return all([self.store_conf.get('cinder_store_' + key)
for key in ['user_name', 'password',
'project_name', 'auth_address']])
def get_cinderclient(self, context=None):
if self.backend_group:
glance_store = getattr(self.conf, self.backend_group)
def get_cinderclient(self, context=None, legacy_update=False):
# NOTE: For legacy image update from single store to multiple
# stores we need to use admin context rather than user provided
# credentials
if legacy_update:
user_overriden = False
context = context.elevated()
else:
glance_store = self.conf.glance_store
user_overriden = self.is_user_overriden()
user_overriden = self.is_user_overriden()
if user_overriden:
username = glance_store.cinder_store_user_name
password = glance_store.cinder_store_password
project = glance_store.cinder_store_project_name
url = glance_store.cinder_store_auth_address
username = self.store_conf.cinder_store_user_name
password = self.store_conf.cinder_store_password
project = self.store_conf.cinder_store_project_name
url = self.store_conf.cinder_store_auth_address
else:
username = context.user
password = context.auth_token
project = context.tenant
if glance_store.cinder_endpoint_template:
url = glance_store.cinder_endpoint_template % context.to_dict()
if self.store_conf.cinder_endpoint_template:
template = self.store_conf.cinder_endpoint_template
url = template % context.to_dict()
else:
info = glance_store.cinder_catalog_info
info = self.store_conf.cinder_catalog_info
service_type, service_name, interface = info.split(':')
try:
catalog = keystone_sc.ServiceCatalogV2(
context.service_catalog)
url = catalog.url_for(
region_name=glance_store.cinder_os_region_name,
region_name=self.store_conf.cinder_os_region_name,
service_type=service_type,
service_name=service_name,
interface=interface)
@ -447,10 +519,10 @@ class Store(glance_store.driver.Store):
c = cinderclient.Client(
username, password, project, auth_url=url,
region_name=glance_store.cinder_os_region_name,
insecure=glance_store.cinder_api_insecure,
retries=glance_store.cinder_http_retries,
cacert=glance_store.cinder_ca_certificates_file)
region_name=self.store_conf.cinder_os_region_name,
insecure=self.store_conf.cinder_api_insecure,
retries=self.store_conf.cinder_http_retries,
cacert=self.store_conf.cinder_ca_certificates_file)
LOG.debug(
'Cinderclient connection created for user %(user)s using URL: '
@ -485,9 +557,6 @@ class Store(glance_store.driver.Store):
def get_schemes(self):
return ('cinder',)
def _set_url_prefix(self):
self._url_prefix = "cinder://"
def _check_context(self, context, require_tenant=False):
user_overriden = self.is_user_overriden()
if user_overriden and not require_tenant:
@ -503,12 +572,7 @@ class Store(glance_store.driver.Store):
def _wait_volume_status(self, volume, status_transition, status_expected):
max_recheck_wait = 15
if self.backend_group:
timeout = getattr(
self.conf, self.backend_group).cinder_state_transition_timeout
else:
timeout = self.conf.glance_store.cinder_state_transition_timeout
timeout = self.store_conf.cinder_state_transition_timeout
volume = volume.manager.get(volume.id)
tries = 0
elapsed = 0
@ -557,17 +621,9 @@ class Store(glance_store.driver.Store):
root_helper = self.get_root_helper()
priv_context.init(root_helper=shlex.split(root_helper))
host = socket.gethostname()
if self.backend_group:
use_multipath = getattr(
self.conf, self.backend_group).cinder_use_multipath
enforce_multipath = getattr(
self.conf, self.backend_group).cinder_enforce_multipath
mount_point_base = getattr(
self.conf, self.backend_group).cinder_mount_point_base
else:
use_multipath = self.conf.glance_store.cinder_use_multipath
enforce_multipath = self.conf.glance_store.cinder_enforce_multipath
mount_point_base = self.conf.glance_store.cinder_mount_point_base
use_multipath = self.store_conf.cinder_use_multipath
enforce_multipath = self.store_conf.cinder_enforce_multipath
mount_point_base = self.store_conf.cinder_mount_point_base
properties = connector.get_connector_properties(
root_helper, host, use_multipath, enforce_multipath)
@ -785,11 +841,7 @@ class Store(glance_store.driver.Store):
'image_size': str(image_size),
'image_owner': owner}
if self.backend_group:
volume_type = getattr(self.conf,
self.backend_group).cinder_volume_type
else:
volume_type = self.conf.glance_store.cinder_volume_type
volume_type = self.store_conf.cinder_volume_type
LOG.debug('Creating a new volume: image_size=%d size_gb=%d type=%s',
image_size, size_gb, volume_type or 'None')
@ -873,10 +925,13 @@ class Store(glance_store.driver.Store):
'checksum_hex': checksum_hex})
image_metadata = {}
location_url = 'cinder://%s' % volume.id
if self.backend_group:
image_metadata['store'] = u"%s" % self.backend_group
location_url = 'cinder://%s/%s' % (self.backend_group,
volume.id)
return ('cinder://%s' % volume.id,
return (location_url,
bytes_written,
checksum_hex,
hash_hex,

View File

@ -408,3 +408,6 @@ class TestCinderStore(base.StoreBaseTest,
loc = location.get_location_from_uri(uri, conf=self.conf)
self.store.delete(loc, context=self.context)
fake_volume.delete.assert_called_once_with()
def test_set_url_prefix(self):
self.assertEqual('cinder://', self.store._url_prefix)

View File

@ -92,9 +92,15 @@ class TestMultiCinderStore(base.MultiStoreBaseTest,
user='fake_user',
auth_token='fake_token',
tenant='fake_tenant')
self.fake_admin_context = mock.MagicMock()
self.fake_admin_context.elevated.return_value = FakeObject(
service_catalog=fake_sc,
user='admin_user',
auth_token='admin_token',
tenant='admin_project')
def test_location_url_prefix_is_set(self):
self.assertEqual("cinder://", self.store.url_prefix)
self.assertEqual("cinder://cinder1", self.store.url_prefix)
def test_get_cinderclient(self):
cc = self.store.get_cinderclient(self.context)
@ -110,6 +116,14 @@ class TestMultiCinderStore(base.MultiStoreBaseTest,
self.assertIsNone(cc.client.auth_token)
self.assertEqual('test_address', cc.client.management_url)
def test_get_cinderclient_legacy_update(self):
cc = self.store.get_cinderclient(self.fake_admin_context,
legacy_update=True)
self.assertEqual('admin_token', cc.client.auth_token)
self.assertEqual('admin_user', cc.client.user)
self.assertEqual('admin_project', cc.client.projectid)
self.assertEqual('http://foo/public_url', cc.client.management_url)
def test_temporary_chown(self):
class fake_stat(object):
st_uid = 1
@ -247,7 +261,7 @@ class TestMultiCinderStore(base.MultiStoreBaseTest,
multipath_supported=True,
enforce_multipath=True)
def test_cinder_configure_add(self):
def test_cinder_check_context(self):
self.assertRaises(exceptions.BadStoreConfiguration,
self.store._check_context, None)
@ -257,6 +271,57 @@ class TestMultiCinderStore(base.MultiStoreBaseTest,
self.store._check_context(FakeObject(service_catalog='fake'))
def test_cinder_configure_add(self):
with mock.patch.object(self.store, 'get_cinderclient') as mocked_cc:
def raise_(ex):
raise ex
mocked_cc.return_value = FakeObject(volume_types=FakeObject(
find=lambda name: 'some_type' if name == 'some_type'
else raise_(cinder.cinder_exception.NotFound(code=404))))
self.config(cinder_volume_type='some_type',
group=self.store.backend_group)
# If volume type exists, no exception is raised
self.store.configure_add()
# setting cinder_volume_type to non-existent value will raise
# BadStoreConfiguration exception
self.config(cinder_volume_type='some_random_type',
group=self.store.backend_group)
self.assertRaises(exceptions.BadStoreConfiguration,
self.store.configure_add)
# when only 1 store is configured, BackendException is raised
self.config(enabled_backends={'cinder1': 'cinder'})
self.assertRaises(exceptions.BackendException,
self.store.configure_add)
def test_is_image_associated_with_store(self):
with mock.patch.object(self.store, 'get_cinderclient') as mocked_cc:
mocked_cc.return_value = FakeObject(volumes=FakeObject(
get=lambda volume_id: FakeObject(volume_type='some_type')),
volume_types=FakeObject(
default=lambda: {'name': 'some_type'}))
# When cinder_volume_type is set and is same as volume's type
self.config(cinder_volume_type='some_type',
group=self.store.backend_group)
fake_vol_id = str(uuid.uuid4())
type_match = self.store.is_image_associated_with_store(
self.context, fake_vol_id)
self.assertTrue(type_match)
# When cinder_volume_type is not set and volume's type is same as
# set default volume type
self.config(cinder_volume_type=None,
group=self.store.backend_group)
type_match = self.store.is_image_associated_with_store(
self.context, fake_vol_id)
self.assertTrue(type_match)
# When cinder_volume_type is not set and volume's type does not
# match with default volume type
mocked_cc.return_value.volume_types = FakeObject(
default=lambda: {'name': 'random_type'})
type_match = self.store.is_image_associated_with_store(
self.context, fake_vol_id)
self.assertFalse(type_match)
def test_cinder_get(self):
expected_size = 5 * units.Ki
expected_file_contents = b"*" * expected_size
@ -279,7 +344,7 @@ class TestMultiCinderStore(base.MultiStoreBaseTest,
side_effect=fake_open):
mock_cc.return_value = FakeObject(client=fake_client,
volumes=fake_volumes)
uri = "cinder://%s" % fake_volume_uuid
uri = "cinder://cinder1/%s" % fake_volume_uuid
loc = location.get_location_from_uri_and_backend(uri,
"cinder1",
conf=self.conf)
@ -306,7 +371,7 @@ class TestMultiCinderStore(base.MultiStoreBaseTest,
mocked_cc.return_value = FakeObject(client=fake_client,
volumes=fake_volumes)
uri = 'cinder://%s' % fake_volume_uuid
uri = 'cinder://cinder1/%s' % fake_volume_uuid
loc = location.get_location_from_uri_and_backend(uri,
"cinder1",
conf=self.conf)
@ -325,7 +390,7 @@ class TestMultiCinderStore(base.MultiStoreBaseTest,
mocked_cc.return_value = FakeObject(client=fake_client,
volumes=fake_volumes)
uri = 'cinder://%s' % fake_volume_uuid
uri = 'cinder://cinder1/%s' % fake_volume_uuid
loc = location.get_location_from_uri_and_backend(uri,
"cinder1",
conf=self.conf)
@ -339,7 +404,7 @@ class TestMultiCinderStore(base.MultiStoreBaseTest,
expected_file_contents = b"*" * expected_size
image_file = six.BytesIO(expected_file_contents)
expected_checksum = hashlib.md5(expected_file_contents).hexdigest()
expected_location = 'cinder://%s' % fake_volume.id
expected_location = 'cinder://%s/%s' % (backend, fake_volume.id)
fake_client = FakeObject(auth_token=None, management_url=None)
fake_volume.manager.get.return_value = fake_volume
fake_volumes = FakeObject(create=mock.Mock(return_value=fake_volume))
@ -410,7 +475,7 @@ class TestMultiCinderStore(base.MultiStoreBaseTest,
mocked_cc.return_value = FakeObject(client=fake_client,
volumes=fake_volumes)
uri = 'cinder://%s' % fake_volume_uuid
uri = 'cinder://cinder1/%s' % fake_volume_uuid
loc = location.get_location_from_uri_and_backend(uri,
"cinder1",
conf=self.conf)

View File

@ -0,0 +1,10 @@
---
features:
- |
Added support for cinder multiple stores. Operators can now configure
multiple cinder stores by configuring a unique cinder_volume_type for
each cinder store.
upgrade:
- |
Legacy images will be moved to specific stores as per their current
volume's type and the location URL will be updated respectively.