Adds support for min/max volume size on vol_type

Allows support for setting a minimum and/or maximum vol size that
can be created in extra_specs for each volume_type. This allows
setting size restrictions on different "tiers" of storage.
If configured, the size restrictions will be checked at the API level
as part of volume creation or retype.

2 new volume type keys are supported for setting the minimum volume
size and maximum volume size for that type.
'provisioning:min_vol_size'
'provisioning:max_vol_size'

Implements: blueprint min-max-vol-size-by-vol-type

Change-Id: I222e778902a41e552e812896d7afd0516ee7fe68
This commit is contained in:
Hemna 2020-03-18 18:05:14 +00:00 committed by Walter A. Boring IV (hemna)
parent 1d3fa89752
commit f26f683c0f
6 changed files with 227 additions and 0 deletions

View File

@ -633,3 +633,49 @@ class VolumeTypeTestCase(test.TestCase):
'volume_type_project.test_suffix', 'volume_type_project.test_suffix',
{'volume_type_id': volume_type_id, {'volume_type_id': volume_type_id,
'project_id': project_id}) 'project_id': project_id})
def test_provision_filter_on_size(self):
volume_types.create(self.ctxt, "type1",
{"key1": "val1", "key2": "val2"})
volume_types.create(self.ctxt, "type2",
{volume_types.MIN_SIZE_KEY: "12",
"key3": "val3"})
volume_types.create(self.ctxt, "type3",
{volume_types.MAX_SIZE_KEY: "99",
"key4": "val4"})
volume_types.create(self.ctxt, "type4",
{volume_types.MIN_SIZE_KEY: "24",
volume_types.MAX_SIZE_KEY: "99",
"key4": "val4"})
# Make sure we don't raise if there are no min/max set
type1 = volume_types.get_by_name_or_id(self.ctxt, 'type1')
volume_types.provision_filter_on_size(self.ctxt, type1, "11")
# verify minimum size requirements
type2 = volume_types.get_by_name_or_id(self.ctxt, 'type2')
self.assertRaises(exception.InvalidInput,
volume_types.provision_filter_on_size,
self.ctxt, type2, "11")
volume_types.provision_filter_on_size(self.ctxt, type2, "12")
volume_types.provision_filter_on_size(self.ctxt, type2, "100")
# verify max size requirements
type3 = volume_types.get_by_name_or_id(self.ctxt, 'type3')
self.assertRaises(exception.InvalidInput,
volume_types.provision_filter_on_size,
self.ctxt, type3, "100")
volume_types.provision_filter_on_size(self.ctxt, type3, "99")
volume_types.provision_filter_on_size(self.ctxt, type3, "1")
# verify min and max
type4 = volume_types.get_by_name_or_id(self.ctxt, 'type4')
self.assertRaises(exception.InvalidInput,
volume_types.provision_filter_on_size,
self.ctxt, type4, "20")
self.assertRaises(exception.InvalidInput,
volume_types.provision_filter_on_size,
self.ctxt, type4, "130")
volume_types.provision_filter_on_size(self.ctxt, type4, "24")
volume_types.provision_filter_on_size(self.ctxt, type4, "99")
volume_types.provision_filter_on_size(self.ctxt, type4, "30")

View File

@ -104,6 +104,7 @@ class VolumeTestCase(base.BaseVolumeTestCase):
v2_fakes.fake_default_type_get( v2_fakes.fake_default_type_get(
id=fake.VOLUME_TYPE2_ID)) id=fake.VOLUME_TYPE2_ID))
self.vol_type = db.volume_type_get_by_name(elevated, '__DEFAULT__') self.vol_type = db.volume_type_get_by_name(elevated, '__DEFAULT__')
self._setup_volume_types()
def _create_volume(self, context, **kwargs): def _create_volume(self, context, **kwargs):
return tests_utils.create_volume( return tests_utils.create_volume(
@ -196,6 +197,26 @@ class VolumeTestCase(base.BaseVolumeTestCase):
self.volume.driver._initialized = False self.volume.driver._initialized = False
self.assertFalse(self.volume.is_working()) self.assertFalse(self.volume.is_working())
def _create_min_max_size_dict(self, min_size, max_size):
return {volume_types.MIN_SIZE_KEY: min_size,
volume_types.MAX_SIZE_KEY: max_size}
def _setup_volume_types(self):
"""Creates 2 types, one with size limits, one without."""
spec_dict = self._create_min_max_size_dict(2, 4)
sized_vol_type_dict = {'name': 'limit',
'extra_specs': spec_dict}
db.volume_type_create(self.context, sized_vol_type_dict)
self.sized_vol_type = db.volume_type_get_by_name(
self.context, sized_vol_type_dict['name'])
unsized_vol_type_dict = {'name': 'unsized', 'extra_specs': {}}
db.volume_type_create(context.get_admin_context(),
unsized_vol_type_dict)
self.unsized_vol_type = db.volume_type_get_by_name(
self.context, unsized_vol_type_dict['name'])
@mock.patch('cinder.tests.unit.fake_notifier.FakeNotifier._notify') @mock.patch('cinder.tests.unit.fake_notifier.FakeNotifier._notify')
@mock.patch.object(QUOTAS, 'reserve') @mock.patch.object(QUOTAS, 'reserve')
@mock.patch.object(QUOTAS, 'commit') @mock.patch.object(QUOTAS, 'commit')
@ -628,6 +649,35 @@ class VolumeTestCase(base.BaseVolumeTestCase):
volume_type=db_vol_type) volume_type=db_vol_type)
self.assertEqual(db_vol_type.get('id'), volume['volume_type_id']) self.assertEqual(db_vol_type.get('id'), volume['volume_type_id'])
@mock.patch('cinder.quota.QUOTAS.rollback', new=mock.MagicMock())
@mock.patch('cinder.quota.QUOTAS.commit', new=mock.MagicMock())
@mock.patch('cinder.quota.QUOTAS.reserve', return_value=["RESERVATION"])
def test_create_volume_with_volume_type_size_limits(self, _mock_reserve):
"""Test that volume type size limits are enforced."""
volume_api = cinder.volume.api.API()
volume = volume_api.create(self.context,
2,
'name',
'description',
volume_type=self.sized_vol_type)
self.assertEqual(self.sized_vol_type['id'], volume['volume_type_id'])
self.assertRaises(exception.InvalidInput,
volume_api.create,
self.context,
1,
'name',
'description',
volume_type=self.sized_vol_type)
self.assertRaises(exception.InvalidInput,
volume_api.create,
self.context,
5,
'name',
'description',
volume_type=self.sized_vol_type)
def test_create_volume_with_multiattach_volume_type(self): def test_create_volume_with_multiattach_volume_type(self):
"""Test volume creation with multiattach volume type.""" """Test volume creation with multiattach volume type."""
elevated = context.get_admin_context() elevated = context.get_admin_context()
@ -2543,6 +2593,26 @@ class VolumeTestCase(base.BaseVolumeTestCase):
# clean up # clean up
self.volume.delete_volume(self.context, volume) self.volume.delete_volume(self.context, volume)
@mock.patch.object(QUOTAS, 'limit_check')
@mock.patch.object(QUOTAS, 'reserve')
def test_extend_volume_with_volume_type_limit(self, reserve, limit_check):
"""Test volume can be extended at API level."""
volume_api = cinder.volume.api.API()
volume = tests_utils.create_volume(
self.context, size=2,
volume_type_id=self.sized_vol_type['id'])
volume_api.scheduler_rpcapi = mock.MagicMock()
volume_api.scheduler_rpcapi.extend_volume = mock.MagicMock()
volume_api._extend(self.context, volume, 3)
self.assertRaises(exception.InvalidInput,
volume_api._extend,
self.context,
volume,
5)
def test_extend_volume_driver_not_initialized(self): def test_extend_volume_driver_not_initialized(self):
"""Test volume can be extended at API level.""" """Test volume can be extended at API level."""
# create a volume and assign to host # create a volume and assign to host

View File

@ -16,6 +16,7 @@ from unittest import mock
from oslo_config import cfg from oslo_config import cfg
from cinder import context from cinder import context
from cinder import db
from cinder import exception from cinder import exception
from cinder import objects from cinder import objects
from cinder.policies import volume_actions as vol_action_policies from cinder.policies import volume_actions as vol_action_policies
@ -167,3 +168,56 @@ class VolumeRetypeTestCase(base.BaseVolumeTestCase):
volume.refresh() volume.refresh()
self.assertEqual('available', volume.status) self.assertEqual('available', volume.status)
def test_retype_with_volume_type_resize_limits(self):
def _create_min_max_size_dict(min_size, max_size):
return {volume_types.MIN_SIZE_KEY: min_size,
volume_types.MAX_SIZE_KEY: max_size}
def _setup_volume_types():
spec_dict = _create_min_max_size_dict(2, 4)
sized_vol_type_dict = {'name': 'limit_type',
'extra_specs': spec_dict}
db.volume_type_create(self.context, sized_vol_type_dict)
self.sized_vol_type = db.volume_type_get_by_name(
self.context, sized_vol_type_dict['name'])
unsized_vol_type_dict = {'name': 'unsized_type', 'extra_specs': {}}
db.volume_type_create(context.get_admin_context(),
unsized_vol_type_dict)
self.unsized_vol_type = db.volume_type_get_by_name(
self.context, unsized_vol_type_dict['name'])
_setup_volume_types()
volume_1 = tests_utils.create_volume(
self.context,
host=CONF.host,
status='available',
volume_type_id=self.default_vol_type.id,
size=1)
volume_3 = tests_utils.create_volume(
self.context,
host=CONF.host,
status='available',
volume_type_id=self.default_vol_type.id,
size=3)
volume_9 = tests_utils.create_volume(
self.context,
host=CONF.host,
status='available',
volume_type_id=self.default_vol_type.id,
size=9)
self.assertRaises(exception.InvalidInput,
self.volume_api.retype,
self.context, volume_1,
'limit_type',
migration_policy='on-demand')
self.assertRaises(exception.InvalidInput,
self.volume_api.retype,
self.context, volume_9,
'limit_type',
migration_policy='on-demand')
self.volume_api.retype(self.context, volume_3,
'limit_type', migration_policy='on-demand')

View File

@ -242,6 +242,9 @@ class API(base.Base):
'than zero).') % size 'than zero).') % size
raise exception.InvalidInput(reason=msg) raise exception.InvalidInput(reason=msg)
# ensure we pass the volume_type provisioning filter on size
volume_types.provision_filter_on_size(context, volume_type, size)
if consistencygroup and (not cgsnapshot and not source_cg): if consistencygroup and (not cgsnapshot and not source_cg):
if not volume_type: if not volume_type:
msg = _("volume_type must be provided when creating " msg = _("volume_type must be provided when creating "
@ -1389,6 +1392,14 @@ class API(base.Base):
'size': volume.size}) 'size': volume.size})
raise exception.InvalidInput(reason=msg) raise exception.InvalidInput(reason=msg)
# Make sure we pass the potential size limitations in the volume type
try:
volume_type = volume_types.get_volume_type(context,
volume.volume_type_id)
except (exception.InvalidVolumeType, exception.VolumeTypeNotFound):
volume_type = None
volume_types.provision_filter_on_size(context, volume_type, new_size)
result = volume.conditional_update(value, expected) result = volume.conditional_update(value, expected)
if not result: if not result:
msg = (_("Volume %(vol_id)s status must be '%(expected)s' " msg = (_("Volume %(vol_id)s status must be '%(expected)s' "
@ -1635,6 +1646,9 @@ class API(base.Base):
new_type_id = new_type['id'] new_type_id = new_type['id']
# Make sure we pass the potential size limitations in the volume type
volume_types.provision_filter_on_size(context, new_type, volume.size)
# NOTE(jdg): We check here if multiattach is involved in either side # NOTE(jdg): We check here if multiattach is involved in either side
# of the retype, we can't change multiattach on an in-use volume # of the retype, we can't change multiattach on an in-use volume
# because there's things the hypervisor needs when attaching, so # because there's things the hypervisor needs when attaching, so

View File

@ -40,6 +40,9 @@ ENCRYPTION_IGNORED_FIELDS = ['volume_type_id', 'created_at', 'updated_at',
'deleted_at', 'encryption_id'] 'deleted_at', 'encryption_id']
DEFAULT_VOLUME_TYPE = "__DEFAULT__" DEFAULT_VOLUME_TYPE = "__DEFAULT__"
MIN_SIZE_KEY = "provisioning:min_vol_size"
MAX_SIZE_KEY = "provisioning:max_vol_size"
def create(context, def create(context,
name, name,
@ -400,3 +403,38 @@ def volume_types_encryption_changed(context, vol_type_id1, vol_type_id2):
enc1_filtered = _get_encryption(enc1) if enc1 else None enc1_filtered = _get_encryption(enc1) if enc1 else None
enc2_filtered = _get_encryption(enc2) if enc2 else None enc2_filtered = _get_encryption(enc2) if enc2 else None
return enc1_filtered != enc2_filtered return enc1_filtered != enc2_filtered
def provision_filter_on_size(context, volume_type, size):
"""This function filters volume provisioning requests on size limits.
If a volume type has provisioning size min/max set, this filter
will ensure that the volume size requested is within the size
limits specified in the volume type.
"""
if not volume_type:
volume_type = get_default_volume_type()
if volume_type:
size_int = int(size)
extra_specs = volume_type.get('extra_specs', {})
min_size = extra_specs.get(MIN_SIZE_KEY)
if min_size and size_int < int(min_size):
msg = _("Specified volume size of '%(req_size)d' is less "
"than the minimum required size of '%(min_size)s' "
"for volume type '%(vol_type)s'.") % {
'req_size': size_int, 'min_size': min_size,
'vol_type': volume_type['name']
}
raise exception.InvalidInput(reason=msg)
max_size = extra_specs.get(MAX_SIZE_KEY)
if max_size and size_int > int(max_size):
msg = _("Specified volume size of '%(req_size)d' is "
"greater than the maximum allowable size of "
"'%(max_size)s' for volume type '%(vol_type)s'."
) % {
'req_size': size_int, 'max_size': max_size,
'vol_type': volume_type['name']}
raise exception.InvalidInput(reason=msg)

View File

@ -0,0 +1,5 @@
---
features:
- Ability to add minimum and maximum volume size restrictions which
can be set on a per volume-type granularity. New volume type keys of
'provisioning:min_vol_size' and 'provisioning:max_vol_size'.