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:
parent
1d3fa89752
commit
f26f683c0f
@ -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")
|
||||
|
@ -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
|
||||
|
@ -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')
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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'.
|
Loading…
Reference in New Issue
Block a user