Browse Source

Merge "Adds support for min/max volume size on vol_type"

tags/16.0.0.0rc1
Zuul 2 months ago
committed by Gerrit Code Review
parent
commit
f01bfeead9
6 changed files with 227 additions and 0 deletions
  1. +46
    -0
      cinder/tests/unit/test_volume_types.py
  2. +70
    -0
      cinder/tests/unit/volume/test_volume.py
  3. +54
    -0
      cinder/tests/unit/volume/test_volume_retype.py
  4. +14
    -0
      cinder/volume/api.py
  5. +38
    -0
      cinder/volume/volume_types.py
  6. +5
    -0
      releasenotes/notes/min-max-vol-size-on-type-bc7c75ea73a74d02.yaml

+ 46
- 0
cinder/tests/unit/test_volume_types.py 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")

+ 70
- 0
cinder/tests/unit/volume/test_volume.py 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


+ 54
- 0
cinder/tests/unit/volume/test_volume_retype.py 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')

+ 14
- 0
cinder/volume/api.py 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


+ 38
- 0
cinder/volume/volume_types.py 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)

+ 5
- 0
releasenotes/notes/min-max-vol-size-on-type-bc7c75ea73a74d02.yaml 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'.

Loading…
Cancel
Save