Add support volume local cache
Use volume type extra spec 'cacheable' to identify if a volume can be cached or not. If it is set to '<is> True', then it means the volume can be cached in compute node locally via caching software (e.g. open-cas) open-cas: https://open-cas.github.io/ Nova Spec: https://review.opendev.org/#/c/689070/ Cinder Spec: https://review.opendev.org/#/c/684556/ Change-Id: I61a795f4ab2c4208094245bd577c39de1b35d6ef Signed-off-by: Liang Fang <liang.a.fang@intel.com>
This commit is contained in:
parent
61e24eb8ab
commit
afcaf0b9dd
@ -63,6 +63,20 @@ class VolumeTypeExtraSpecsController(wsgi.Controller):
|
|||||||
raise webob.exc.HTTPBadRequest(explanation=expl)
|
raise webob.exc.HTTPBadRequest(explanation=expl)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
def _check_cacheable(self, specs, type_id):
|
||||||
|
multiattach = volume_types.get_volume_type_extra_specs(
|
||||||
|
type_id, key='multiattach')
|
||||||
|
cacheable = volume_types.get_volume_type_extra_specs(
|
||||||
|
type_id, key='cacheable')
|
||||||
|
isTrue = '<is> True'
|
||||||
|
if (specs.get('multiattach') == isTrue and cacheable == isTrue) or (
|
||||||
|
specs.get('cacheable') == isTrue and multiattach ==
|
||||||
|
isTrue) or (specs.get('cacheable') == isTrue and
|
||||||
|
specs.get('multiattach') == isTrue):
|
||||||
|
expl = _('cacheable cannot be set with multiattach.')
|
||||||
|
raise webob.exc.HTTPBadRequest(explanation=expl)
|
||||||
|
return
|
||||||
|
|
||||||
@validation.schema(types_extra_specs.create)
|
@validation.schema(types_extra_specs.create)
|
||||||
def create(self, req, type_id, body):
|
def create(self, req, type_id, body):
|
||||||
context = req.environ['cinder.context']
|
context = req.environ['cinder.context']
|
||||||
@ -76,6 +90,9 @@ class VolumeTypeExtraSpecsController(wsgi.Controller):
|
|||||||
image_service_store_id = specs['image_service:store_id']
|
image_service_store_id = specs['image_service:store_id']
|
||||||
image_utils.validate_stores_id(context, image_service_store_id)
|
image_utils.validate_stores_id(context, image_service_store_id)
|
||||||
|
|
||||||
|
# Check if multiattach be set with cacheable
|
||||||
|
self._check_cacheable(specs, type_id)
|
||||||
|
|
||||||
db.volume_type_extra_specs_update_or_create(context,
|
db.volume_type_extra_specs_update_or_create(context,
|
||||||
type_id,
|
type_id,
|
||||||
specs)
|
specs)
|
||||||
@ -104,6 +121,11 @@ class VolumeTypeExtraSpecsController(wsgi.Controller):
|
|||||||
image_service_store_id = body['image_service:store_id']
|
image_service_store_id = body['image_service:store_id']
|
||||||
image_utils.validate_stores_id(context, image_service_store_id)
|
image_utils.validate_stores_id(context, image_service_store_id)
|
||||||
|
|
||||||
|
if 'extra_specs' in body:
|
||||||
|
specs = body['extra_specs']
|
||||||
|
# Check if multiattach be set with cacheable
|
||||||
|
self._check_cacheable(specs, type_id)
|
||||||
|
|
||||||
db.volume_type_extra_specs_update_or_create(context,
|
db.volume_type_extra_specs_update_or_create(context,
|
||||||
type_id,
|
type_id,
|
||||||
body)
|
body)
|
||||||
|
@ -454,3 +454,56 @@ class VolumeTypesExtraSpecsTest(test.TestCase):
|
|||||||
self.assertRaises(exception.ValidationError,
|
self.assertRaises(exception.ValidationError,
|
||||||
self.controller.create, req, fake.VOLUME_ID,
|
self.controller.create, req, fake.VOLUME_ID,
|
||||||
body=body)
|
body=body)
|
||||||
|
|
||||||
|
@mock.patch('cinder.volume.volume_types.get_volume_type_extra_specs')
|
||||||
|
def test_check_cacheable(self, get_extra_specs):
|
||||||
|
ret_multiattach = ''
|
||||||
|
ret_cacheable = ''
|
||||||
|
|
||||||
|
def side_get_specs(type_id, key):
|
||||||
|
if key == 'multiattach':
|
||||||
|
return ret_multiattach
|
||||||
|
if key == 'cacheable':
|
||||||
|
return ret_cacheable
|
||||||
|
get_extra_specs.return_value = ''
|
||||||
|
get_extra_specs.side_effect = side_get_specs
|
||||||
|
|
||||||
|
specs = {'multiattach': '<is> True',
|
||||||
|
'cacheable': '<is> True'}
|
||||||
|
self.assertRaises(webob.exc.HTTPBadRequest,
|
||||||
|
self.controller._check_cacheable,
|
||||||
|
specs, 'typeid')
|
||||||
|
|
||||||
|
ret_multiattach = '<is> True'
|
||||||
|
ret_cacheable = ''
|
||||||
|
specs = {'cacheable': '<is> True'}
|
||||||
|
self.assertRaises(webob.exc.HTTPBadRequest,
|
||||||
|
self.controller._check_cacheable,
|
||||||
|
specs, 'typeid')
|
||||||
|
|
||||||
|
ret_multiattach = ''
|
||||||
|
ret_cacheable = '<is> True'
|
||||||
|
specs = {'multiattach': '<is> True'}
|
||||||
|
self.assertRaises(webob.exc.HTTPBadRequest,
|
||||||
|
self.controller._check_cacheable,
|
||||||
|
specs, 'typeid')
|
||||||
|
|
||||||
|
ret_multiattach = '<is> False'
|
||||||
|
ret_cacheable = ''
|
||||||
|
specs = {'multiattach': '<is> True'}
|
||||||
|
# Should NOT has exception when calling below line
|
||||||
|
self.controller._check_cacheable(specs, 'typeid')
|
||||||
|
|
||||||
|
ret_multiattach = '<is> True'
|
||||||
|
ret_cacheable = ''
|
||||||
|
specs = {'multiattach': '<is> False', 'cacheable': '<is> True'}
|
||||||
|
# Should NOT setting both at the same time
|
||||||
|
self.assertRaises(webob.exc.HTTPBadRequest,
|
||||||
|
self.controller._check_cacheable,
|
||||||
|
specs, 'typeid')
|
||||||
|
|
||||||
|
ret_multiattach = '<is> False'
|
||||||
|
ret_cacheable = ''
|
||||||
|
specs = {'multiattach': '<is> False', 'cacheable': '<is> True'}
|
||||||
|
# Should NOT has exception when calling below line
|
||||||
|
self.controller._check_cacheable(specs, 'typeid')
|
||||||
|
@ -48,7 +48,8 @@ class AttachmentManagerTestCase(test.TestCase):
|
|||||||
@mock.patch.object(db.sqlalchemy.api, '_volume_type_get',
|
@mock.patch.object(db.sqlalchemy.api, '_volume_type_get',
|
||||||
v2_fakes.fake_volume_type_get)
|
v2_fakes.fake_volume_type_get)
|
||||||
@mock.patch('cinder.db.sqlalchemy.api.volume_type_qos_specs_get')
|
@mock.patch('cinder.db.sqlalchemy.api.volume_type_qos_specs_get')
|
||||||
def test_attachment_update(self, mock_type_get):
|
@mock.patch('cinder.volume.volume_types.get_volume_type_extra_specs')
|
||||||
|
def test_attachment_update(self, get_extra_specs, mock_type_get):
|
||||||
"""Test attachment_update."""
|
"""Test attachment_update."""
|
||||||
volume_params = {'status': 'available'}
|
volume_params = {'status': 'available'}
|
||||||
connector = {
|
connector = {
|
||||||
@ -74,10 +75,12 @@ class AttachmentManagerTestCase(test.TestCase):
|
|||||||
expected = {
|
expected = {
|
||||||
'encrypted': False,
|
'encrypted': False,
|
||||||
'qos_specs': None,
|
'qos_specs': None,
|
||||||
|
'cacheable': False,
|
||||||
'access_mode': 'rw',
|
'access_mode': 'rw',
|
||||||
'driver_volume_type': 'iscsi',
|
'driver_volume_type': 'iscsi',
|
||||||
'attachment_id': attachment_ref.id}
|
'attachment_id': attachment_ref.id}
|
||||||
|
|
||||||
|
get_extra_specs.return_value = ''
|
||||||
self.assertEqual(expected,
|
self.assertEqual(expected,
|
||||||
self.manager.attachment_update(
|
self.manager.attachment_update(
|
||||||
self.context,
|
self.context,
|
||||||
@ -89,6 +92,21 @@ class AttachmentManagerTestCase(test.TestCase):
|
|||||||
attachment_ref.instance_uuid,
|
attachment_ref.instance_uuid,
|
||||||
connector['host'],
|
connector['host'],
|
||||||
"na")
|
"na")
|
||||||
|
expected = {
|
||||||
|
'encrypted': False,
|
||||||
|
'qos_specs': None,
|
||||||
|
'cacheable': True,
|
||||||
|
'access_mode': 'rw',
|
||||||
|
'driver_volume_type': 'iscsi',
|
||||||
|
'attachment_id': attachment_ref.id}
|
||||||
|
|
||||||
|
get_extra_specs.return_value = '<is> True'
|
||||||
|
self.assertEqual(expected,
|
||||||
|
self.manager.attachment_update(
|
||||||
|
self.context,
|
||||||
|
vref,
|
||||||
|
connector,
|
||||||
|
attachment_ref.id))
|
||||||
|
|
||||||
new_attachment_ref = db.volume_attachment_get(self.context,
|
new_attachment_ref = db.volume_attachment_get(self.context,
|
||||||
attachment_ref.id)
|
attachment_ref.id)
|
||||||
|
@ -152,9 +152,13 @@ class VolumeConnectionTestCase(base.BaseVolumeTestCase):
|
|||||||
|
|
||||||
with mock.patch.object(cinder.volume.volume_types,
|
with mock.patch.object(cinder.volume.volume_types,
|
||||||
'get_volume_type_qos_specs') as type_qos, \
|
'get_volume_type_qos_specs') as type_qos, \
|
||||||
|
mock.patch.object(cinder.volume.volume_types,
|
||||||
|
'get_volume_type_extra_specs'
|
||||||
|
) as type_extra_specs, \
|
||||||
mock.patch.object(cinder.tests.fake_driver.FakeLoggingVolumeDriver,
|
mock.patch.object(cinder.tests.fake_driver.FakeLoggingVolumeDriver,
|
||||||
'initialize_connection') as driver_init:
|
'initialize_connection') as driver_init:
|
||||||
type_qos.return_value = dict(qos_specs=qos_values)
|
type_qos.return_value = dict(qos_specs=qos_values)
|
||||||
|
type_extra_specs.return_value = 'True'
|
||||||
driver_init.return_value = {'data': {}}
|
driver_init.return_value = {'data': {}}
|
||||||
mock_get_target.return_value = None
|
mock_get_target.return_value = None
|
||||||
qos_specs_expected = {'key1': 'value1',
|
qos_specs_expected = {'key1': 'value1',
|
||||||
@ -218,9 +222,13 @@ class VolumeConnectionTestCase(base.BaseVolumeTestCase):
|
|||||||
|
|
||||||
with mock.patch.object(cinder.volume.volume_types,
|
with mock.patch.object(cinder.volume.volume_types,
|
||||||
'get_volume_type_qos_specs') as type_qos, \
|
'get_volume_type_qos_specs') as type_qos, \
|
||||||
|
mock.patch.object(cinder.volume.volume_types,
|
||||||
|
'get_volume_type_extra_specs'
|
||||||
|
) as type_extra_specs, \
|
||||||
mock.patch.object(cinder.tests.fake_driver.FakeLoggingVolumeDriver,
|
mock.patch.object(cinder.tests.fake_driver.FakeLoggingVolumeDriver,
|
||||||
'initialize_connection') as driver_init:
|
'initialize_connection') as driver_init:
|
||||||
type_qos.return_value = dict(qos_specs=qos_values)
|
type_qos.return_value = dict(qos_specs=qos_values)
|
||||||
|
type_extra_specs.return_value = 'True'
|
||||||
driver_init.return_value = {'data': {}}
|
driver_init.return_value = {'data': {}}
|
||||||
mock_get_target.return_value = None
|
mock_get_target.return_value = None
|
||||||
qos_specs_expected = {'write_iops_sec': 90,
|
qos_specs_expected = {'write_iops_sec': 90,
|
||||||
@ -286,9 +294,13 @@ class VolumeConnectionTestCase(base.BaseVolumeTestCase):
|
|||||||
|
|
||||||
with mock.patch.object(cinder.volume.volume_types,
|
with mock.patch.object(cinder.volume.volume_types,
|
||||||
'get_volume_type_qos_specs') as type_qos, \
|
'get_volume_type_qos_specs') as type_qos, \
|
||||||
|
mock.patch.object(cinder.volume.volume_types,
|
||||||
|
'get_volume_type_extra_specs'
|
||||||
|
) as type_extra_specs, \
|
||||||
mock.patch.object(cinder.tests.fake_driver.FakeLoggingVolumeDriver,
|
mock.patch.object(cinder.tests.fake_driver.FakeLoggingVolumeDriver,
|
||||||
'initialize_connection') as driver_init:
|
'initialize_connection') as driver_init:
|
||||||
type_qos.return_value = dict(qos_specs=qos_values)
|
type_qos.return_value = dict(qos_specs=qos_values)
|
||||||
|
type_extra_specs.return_value = 'True'
|
||||||
driver_init.return_value = {'data': {}}
|
driver_init.return_value = {'data': {}}
|
||||||
mock_get_target.return_value = None
|
mock_get_target.return_value = None
|
||||||
qos_specs_expected = {'write_iops_sec': 800,
|
qos_specs_expected = {'write_iops_sec': 800,
|
||||||
@ -354,9 +366,13 @@ class VolumeConnectionTestCase(base.BaseVolumeTestCase):
|
|||||||
|
|
||||||
with mock.patch.object(cinder.volume.volume_types,
|
with mock.patch.object(cinder.volume.volume_types,
|
||||||
'get_volume_type_qos_specs') as type_qos, \
|
'get_volume_type_qos_specs') as type_qos, \
|
||||||
|
mock.patch.object(cinder.volume.volume_types,
|
||||||
|
'get_volume_type_extra_specs'
|
||||||
|
) as type_extra_specs, \
|
||||||
mock.patch.object(cinder.tests.fake_driver.FakeLoggingVolumeDriver,
|
mock.patch.object(cinder.tests.fake_driver.FakeLoggingVolumeDriver,
|
||||||
'initialize_connection') as driver_init:
|
'initialize_connection') as driver_init:
|
||||||
type_qos.return_value = dict(qos_specs=qos_values)
|
type_qos.return_value = dict(qos_specs=qos_values)
|
||||||
|
type_extra_specs.return_value = 'True'
|
||||||
driver_init.return_value = {'data': {}}
|
driver_init.return_value = {'data': {}}
|
||||||
mock_get_target.return_value = None
|
mock_get_target.return_value = None
|
||||||
qos_specs_expected = {'write_iops_sec': 3000,
|
qos_specs_expected = {'write_iops_sec': 3000,
|
||||||
|
@ -172,6 +172,8 @@ class VolumeTestCase(base.BaseVolumeTestCase):
|
|||||||
myfilterfunction = "myFilterFunction"
|
myfilterfunction = "myFilterFunction"
|
||||||
mygoodnessfunction = "myGoodnessFunction"
|
mygoodnessfunction = "myGoodnessFunction"
|
||||||
expected = {'name': 'cinder-volumes',
|
expected = {'name': 'cinder-volumes',
|
||||||
|
'storage_protocol': 'iSCSI',
|
||||||
|
'cacheable': True,
|
||||||
'filter_function': myfilterfunction,
|
'filter_function': myfilterfunction,
|
||||||
'goodness_function': mygoodnessfunction,
|
'goodness_function': mygoodnessfunction,
|
||||||
}
|
}
|
||||||
@ -181,7 +183,9 @@ class VolumeTestCase(base.BaseVolumeTestCase):
|
|||||||
'get_goodness_function') as m_get_goodness:
|
'get_goodness_function') as m_get_goodness:
|
||||||
with mock.patch.object(manager.driver,
|
with mock.patch.object(manager.driver,
|
||||||
'get_filter_function') as m_get_filter:
|
'get_filter_function') as m_get_filter:
|
||||||
m_get_stats.return_value = {'name': 'cinder-volumes'}
|
m_get_stats.return_value = {'name': 'cinder-volumes',
|
||||||
|
'storage_protocol': 'iSCSI',
|
||||||
|
}
|
||||||
m_get_filter.return_value = myfilterfunction
|
m_get_filter.return_value = myfilterfunction
|
||||||
m_get_goodness.return_value = mygoodnessfunction
|
m_get_goodness.return_value = mygoodnessfunction
|
||||||
manager._report_driver_status(context.get_admin_context())
|
manager._report_driver_status(context.get_admin_context())
|
||||||
|
@ -18,6 +18,7 @@ from unittest import mock
|
|||||||
|
|
||||||
from cinder import exception
|
from cinder import exception
|
||||||
from cinder.message import message_field
|
from cinder.message import message_field
|
||||||
|
from cinder.tests.unit import fake_constants as fake
|
||||||
from cinder.tests.unit import fake_volume
|
from cinder.tests.unit import fake_volume
|
||||||
from cinder.tests.unit import volume as base
|
from cinder.tests.unit import volume as base
|
||||||
from cinder.volume import manager as vol_manager
|
from cinder.volume import manager as vol_manager
|
||||||
@ -245,3 +246,44 @@ class VolumeManagerTestCase(base.BaseVolumeTestCase):
|
|||||||
mock_detach.assert_called_once_with(
|
mock_detach.assert_called_once_with(
|
||||||
ctxt, mock_connect.return_value, vol, mock.sentinel.properties,
|
ctxt, mock_connect.return_value, vol, mock.sentinel.properties,
|
||||||
force=True, remote=mock.sentinel.remote)
|
force=True, remote=mock.sentinel.remote)
|
||||||
|
|
||||||
|
@mock.patch('cinder.volume.volume_types.get_volume_type_extra_specs')
|
||||||
|
@mock.patch('cinder.volume.volume_types.get_volume_type_qos_specs',
|
||||||
|
return_value={'qos_specs': None})
|
||||||
|
def test_parse_connection_options_cacheable(self,
|
||||||
|
mock_get_qos,
|
||||||
|
mock_get_extra_specs):
|
||||||
|
ctxt = mock.Mock()
|
||||||
|
manager = vol_manager.VolumeManager()
|
||||||
|
vol = fake_volume.fake_volume_obj(ctxt)
|
||||||
|
vol.volume_type_id = fake.VOLUME_TYPE_ID
|
||||||
|
|
||||||
|
# no 'cacheable' set by driver, should be extra spec
|
||||||
|
conn_info = {"data": {}}
|
||||||
|
mock_get_extra_specs.return_value = '<is> True'
|
||||||
|
manager._parse_connection_options(ctxt, vol, conn_info)
|
||||||
|
self.assertIn('cacheable', conn_info['data'])
|
||||||
|
self.assertIs(conn_info['data']['cacheable'], True)
|
||||||
|
|
||||||
|
# driver sets 'cacheable' False, should override extra spec
|
||||||
|
conn_info = {"data": {"cacheable": False}}
|
||||||
|
mock_get_extra_specs.return_value = '<is> True'
|
||||||
|
manager._parse_connection_options(ctxt, vol, conn_info)
|
||||||
|
self.assertIn('cacheable', conn_info['data'])
|
||||||
|
self.assertIs(conn_info['data']['cacheable'], False)
|
||||||
|
|
||||||
|
# driver sets 'cacheable' True, nothing in extra spec,
|
||||||
|
# extra spec should override driver
|
||||||
|
conn_info = {"data": {"cacheable": True}}
|
||||||
|
mock_get_extra_specs.return_value = None
|
||||||
|
manager._parse_connection_options(ctxt, vol, conn_info)
|
||||||
|
self.assertIn('cacheable', conn_info['data'])
|
||||||
|
self.assertIs(conn_info['data']['cacheable'], False)
|
||||||
|
|
||||||
|
# driver sets 'cacheable' True, extra spec has False,
|
||||||
|
# extra spec should override driver
|
||||||
|
conn_info = {"data": {"cacheable": True}}
|
||||||
|
mock_get_extra_specs.return_value = '<is> False'
|
||||||
|
manager._parse_connection_options(ctxt, vol, conn_info)
|
||||||
|
self.assertIn('cacheable', conn_info['data'])
|
||||||
|
self.assertIs(conn_info['data']['cacheable'], False)
|
||||||
|
@ -1464,6 +1464,13 @@ class BaseVD(object):
|
|||||||
def initialize_connection(self, volume, connector):
|
def initialize_connection(self, volume, connector):
|
||||||
"""Allow connection to connector and return connection info.
|
"""Allow connection to connector and return connection info.
|
||||||
|
|
||||||
|
..note::
|
||||||
|
Whether or not a volume is 'cacheable' for volume local cache on
|
||||||
|
the hypervisor is normally configured in the volume-type
|
||||||
|
extra-specs. Support may be disabled at the driver level, however,
|
||||||
|
by returning "cacheable": False in the conn_info. This will
|
||||||
|
override any setting in the volume-type extra-specs.
|
||||||
|
|
||||||
:param volume: The volume to be attached
|
:param volume: The volume to be attached
|
||||||
:param connector: Dictionary containing information about what is being
|
:param connector: Dictionary containing information about what is being
|
||||||
connected to.
|
connected to.
|
||||||
|
@ -1760,6 +1760,18 @@ class VolumeManager(manager.CleanableManager,
|
|||||||
encrypted = bool(volume.encryption_key_id)
|
encrypted = bool(volume.encryption_key_id)
|
||||||
conn_info['data']['encrypted'] = encrypted
|
conn_info['data']['encrypted'] = encrypted
|
||||||
|
|
||||||
|
# Add cacheable flag to connection_info if not set in the driver.
|
||||||
|
if typeid:
|
||||||
|
cacheable = volume_types.get_volume_type_extra_specs(
|
||||||
|
typeid, key='cacheable')
|
||||||
|
if conn_info['data'].get('cacheable') is not None:
|
||||||
|
driver_setting = bool(conn_info['data']['cacheable'])
|
||||||
|
# override a True driver_setting but respect False
|
||||||
|
conn_info['data']['cacheable'] = (driver_setting and
|
||||||
|
(cacheable == '<is> True'))
|
||||||
|
else:
|
||||||
|
conn_info['data']['cacheable'] = (cacheable == '<is> True')
|
||||||
|
|
||||||
# Add discard flag to connection_info if not set in the driver and
|
# Add discard flag to connection_info if not set in the driver and
|
||||||
# configured to be reported.
|
# configured to be reported.
|
||||||
if conn_info['data'].get('discard') is None:
|
if conn_info['data'].get('discard') is None:
|
||||||
@ -2613,6 +2625,18 @@ class VolumeManager(manager.CleanableManager,
|
|||||||
# Append volume stats with 'allocated_capacity_gb'
|
# Append volume stats with 'allocated_capacity_gb'
|
||||||
self._append_volume_stats(volume_stats)
|
self._append_volume_stats(volume_stats)
|
||||||
|
|
||||||
|
# Append cacheable flag for iSCSI/FC/NVMe-oF and only when
|
||||||
|
# cacheable is not set in driver level
|
||||||
|
if volume_stats['storage_protocol'] in [
|
||||||
|
'iSCSI', 'FC', 'NVMe-oF']:
|
||||||
|
if volume_stats.get('pools'):
|
||||||
|
for pool in volume_stats.get('pools'):
|
||||||
|
if pool.get('cacheable') is None:
|
||||||
|
pool['cacheable'] = True
|
||||||
|
else:
|
||||||
|
if volume_stats.get('cacheable') is None:
|
||||||
|
volume_stats['cacheable'] = True
|
||||||
|
|
||||||
# Append filter and goodness function if needed
|
# Append filter and goodness function if needed
|
||||||
volume_stats = (
|
volume_stats = (
|
||||||
self._append_filter_goodness_functions(volume_stats))
|
self._append_filter_goodness_functions(volume_stats))
|
||||||
|
Loading…
Reference in New Issue
Block a user