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_id': volume_type_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(
id=fake.VOLUME_TYPE2_ID))
self.vol_type = db.volume_type_get_by_name(elevated, '__DEFAULT__')
self._setup_volume_types()
def _create_volume(self, context, **kwargs):
return tests_utils.create_volume(
@ -196,6 +197,26 @@ class VolumeTestCase(base.BaseVolumeTestCase):
self.volume.driver._initialized = False
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.object(QUOTAS, 'reserve')
@mock.patch.object(QUOTAS, 'commit')
@ -628,6 +649,35 @@ class VolumeTestCase(base.BaseVolumeTestCase):
volume_type=db_vol_type)
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):
"""Test volume creation with multiattach volume type."""
elevated = context.get_admin_context()
@ -2543,6 +2593,26 @@ class VolumeTestCase(base.BaseVolumeTestCase):
# clean up
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):
"""Test volume can be extended at API level."""
# create a volume and assign to host

View File

@ -16,6 +16,7 @@ from unittest import mock
from oslo_config import cfg
from cinder import context
from cinder import db
from cinder import exception
from cinder import objects
from cinder.policies import volume_actions as vol_action_policies
@ -167,3 +168,56 @@ class VolumeRetypeTestCase(base.BaseVolumeTestCase):
volume.refresh()
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
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 not volume_type:
msg = _("volume_type must be provided when creating "
@ -1389,6 +1392,14 @@ class API(base.Base):
'size': volume.size})
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)
if not result:
msg = (_("Volume %(vol_id)s status must be '%(expected)s' "
@ -1635,6 +1646,9 @@ class API(base.Base):
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
# of the retype, we can't change multiattach on an in-use volume
# 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']
DEFAULT_VOLUME_TYPE = "__DEFAULT__"
MIN_SIZE_KEY = "provisioning:min_vol_size"
MAX_SIZE_KEY = "provisioning:max_vol_size"
def create(context,
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
enc2_filtered = _get_encryption(enc2) if enc2 else None
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'.