Adds support min/max share size limited by share_type
Allows set min/max share size that can be created in extra_specs for each share_type.the share size will be checked at API level as part of share create, extend, shrink, migration_start. when manage share, check it after get true size of share at manager layer. new extra_specs keys are supported for set min/max size of share. 'provisioning:max_share_size' 'provisioning:min_share_size' Implements: blueprint share-size-limited-by-share-type Change-Id: I5ce0fabf59bfca5ebaf0be5ffe9986e2b0480295
This commit is contained in:
parent
067d44aab6
commit
74415f6d4a
@ -257,3 +257,13 @@ Share type common capability extra-specs that are not visible to end users:
|
||||
can be accessed via IPv6 protocol. If administrators do not set this
|
||||
capability as an extra-spec in a share type, the scheduler can place new
|
||||
shares of that type in pools without regard for whether IPv6 is supported.
|
||||
|
||||
* **provisioning:max_share_size** can set the max size of share, the value
|
||||
must be integers and greater than 0. If administrators set this capability
|
||||
as an extra-spec in a share type, the size of share created with the share
|
||||
type can not greater than the specified value.
|
||||
|
||||
* **provisioning:min_share_size** can set the min size of share, the value
|
||||
must be integers and greater than 0. If administrators set this capability
|
||||
as an extra-spec in a share type, the size of share created with the share
|
||||
type can not less than the specified value.
|
||||
|
@ -482,6 +482,22 @@ def validate_access(*args, **kwargs):
|
||||
raise webob.exc.HTTPBadRequest(explanation=exc_str)
|
||||
|
||||
|
||||
def validate_integer(value, name, min_value=None, max_value=None):
|
||||
"""Make sure that value is a valid integer, potentially within range.
|
||||
|
||||
:param value: the value of the integer
|
||||
:param name: the name of the integer
|
||||
:param min_length: the min_length of the integer
|
||||
:param max_length: the max_length of the integer
|
||||
:returns: integer
|
||||
"""
|
||||
try:
|
||||
value = strutils.validate_integer(value, name, min_value, max_value)
|
||||
return value
|
||||
except ValueError as e:
|
||||
raise webob.exc.HTTPBadRequest(explanation=str(e))
|
||||
|
||||
|
||||
def validate_public_share_policy(context, api_params, api='create'):
|
||||
"""Validates if policy allows is_public parameter to be set to True.
|
||||
|
||||
|
@ -160,13 +160,17 @@ REST_API_VERSION_HISTORY = """
|
||||
view.
|
||||
* 2.59 - Add driver ``details`` field to migration get progress.
|
||||
* 2.60 - API URLs no longer need to include a project_id parameter.
|
||||
* 2.61 - Added optional provisioning:max_share_size and
|
||||
provisioning:min_share_size extra spec,
|
||||
which was add minimum and maximum share size restrictions
|
||||
on a per share-type granularity.
|
||||
"""
|
||||
|
||||
# The minimum and maximum versions of the API supported
|
||||
# The default api version request is defined to be the
|
||||
# minimum version of the API supported.
|
||||
_MIN_API_VERSION = "2.0"
|
||||
_MAX_API_VERSION = "2.60"
|
||||
_MAX_API_VERSION = "2.61"
|
||||
DEFAULT_API_VERSION = _MIN_API_VERSION
|
||||
|
||||
|
||||
|
@ -331,3 +331,9 @@ user documentation.
|
||||
equivalent to https://$(controller)s/share/v2/shares. When interacting
|
||||
with the manila service as system or domain scoped users, project_id should
|
||||
not be specified in the API path.
|
||||
|
||||
2.61
|
||||
----
|
||||
Ability to add minimum and maximum share size restrictions which
|
||||
can be set on a per share-type granularity. Added new extra specs
|
||||
'provisioning:max_share_size' and 'provisioning:min_share_size'.
|
||||
|
@ -235,6 +235,8 @@ class ExtraSpecs(object):
|
||||
REVERT_TO_SNAPSHOT_SUPPORT = "revert_to_snapshot_support"
|
||||
MOUNT_SNAPSHOT_SUPPORT = "mount_snapshot_support"
|
||||
AVAILABILITY_ZONES = "availability_zones"
|
||||
PROVISIONING_MAX_SHARE_SIZE = "provisioning:max_share_size"
|
||||
PROVISIONING_MIN_SHARE_SIZE = "provisioning:min_share_size"
|
||||
|
||||
# Extra specs containers
|
||||
REQUIRED = (
|
||||
@ -248,6 +250,8 @@ class ExtraSpecs(object):
|
||||
REPLICATION_TYPE_SPEC,
|
||||
MOUNT_SNAPSHOT_SUPPORT,
|
||||
AVAILABILITY_ZONES,
|
||||
PROVISIONING_MAX_SHARE_SIZE,
|
||||
PROVISIONING_MIN_SHARE_SIZE
|
||||
)
|
||||
|
||||
# NOTE(cknight): Some extra specs are necessary parts of the Manila API and
|
||||
|
@ -190,6 +190,9 @@ class API(base.Base):
|
||||
"than snapshot size") % size)
|
||||
raise exception.InvalidInput(reason=msg)
|
||||
|
||||
# ensure we pass the share_type provisioning filter on size
|
||||
share_types.provision_filter_on_size(context, share_type, size)
|
||||
|
||||
if snapshot is None:
|
||||
share_type_id = share_type['id'] if share_type else None
|
||||
else:
|
||||
@ -1412,6 +1415,9 @@ class API(base.Base):
|
||||
|
||||
if new_share_type:
|
||||
share_type = new_share_type
|
||||
# ensure pass the size limitations in the share type
|
||||
size = share['size']
|
||||
share_types.provision_filter_on_size(context, share_type, size)
|
||||
new_share_type_id = new_share_type['id']
|
||||
dhss = share_type['extra_specs']['driver_handles_share_servers']
|
||||
dhss = strutils.bool_from_string(dhss, strict=True)
|
||||
@ -2088,6 +2094,14 @@ class API(base.Base):
|
||||
'size': share['size']})
|
||||
raise exception.InvalidInput(reason=msg)
|
||||
|
||||
# ensure we pass the share_type provisioning filter on size
|
||||
try:
|
||||
share_type = share_types.get_share_type(
|
||||
context, share['instance']['share_type_id'])
|
||||
except (exception.InvalidShareType, exception.ShareTypeNotFound):
|
||||
share_type = None
|
||||
share_types.provision_filter_on_size(context, share_type, new_size)
|
||||
|
||||
replicas = self.db.share_replicas_get_all_by_share(
|
||||
context, share['id'])
|
||||
supports_replication = len(replicas) > 0
|
||||
@ -2180,6 +2194,14 @@ class API(base.Base):
|
||||
'size': share['size']})
|
||||
raise exception.InvalidInput(reason=msg)
|
||||
|
||||
# ensure we pass the share_type provisioning filter on size
|
||||
try:
|
||||
share_type = share_types.get_share_type(
|
||||
context, share['instance']['share_type_id'])
|
||||
except (exception.InvalidShareType, exception.ShareTypeNotFound):
|
||||
share_type = None
|
||||
share_types.provision_filter_on_size(context, share_type, new_size)
|
||||
|
||||
self.update(context, share, {'status': constants.STATUS_SHRINKING})
|
||||
self.share_rpcapi.shrink_share(context, share, new_size)
|
||||
LOG.info("Shrink share (id=%(id)s) request issued successfully."
|
||||
|
@ -2566,6 +2566,8 @@ class ShareManager(manager.SchedulerDependentManager):
|
||||
context = context.elevated()
|
||||
share_ref = self.db.share_get(context, share_id)
|
||||
share_instance = self._get_share_instance(context, share_ref)
|
||||
share_type = share_types.get_share_type(
|
||||
context, share_instance['share_type_id'])
|
||||
share_type_extra_specs = self._get_extra_specs_from_share_type(
|
||||
context, share_instance['share_type_id'])
|
||||
share_type_supports_replication = share_type_extra_specs.get(
|
||||
@ -2595,6 +2597,10 @@ class ShareManager(manager.SchedulerDependentManager):
|
||||
if not share_update.get('size'):
|
||||
msg = _("Driver cannot calculate share size.")
|
||||
raise exception.InvalidShare(reason=msg)
|
||||
else:
|
||||
share_types.provision_filter_on_size(context,
|
||||
share_type,
|
||||
share_update.get('size'))
|
||||
|
||||
deltas = {
|
||||
'project_id': project_id,
|
||||
|
@ -24,6 +24,7 @@ from oslo_utils import strutils
|
||||
from oslo_utils import uuidutils
|
||||
import six
|
||||
|
||||
from manila.api import common
|
||||
from manila.common import constants
|
||||
from manila import context
|
||||
from manila import db
|
||||
@ -33,6 +34,9 @@ from manila.i18n import _
|
||||
CONF = cfg.CONF
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
MIN_SIZE_KEY = "provisioning:min_share_size"
|
||||
MAX_SIZE_KEY = "provisioning:max_share_size"
|
||||
|
||||
|
||||
def create(context, name, extra_specs=None, is_public=True,
|
||||
projects=None, description=None):
|
||||
@ -350,6 +354,13 @@ def is_valid_optional_extra_spec(key, value):
|
||||
return parse_boolean_extra_spec(key, value) is not None
|
||||
elif key == constants.ExtraSpecs.AVAILABILITY_ZONES:
|
||||
return is_valid_csv(value)
|
||||
elif key in [constants.ExtraSpecs.PROVISIONING_MAX_SHARE_SIZE,
|
||||
constants.ExtraSpecs.PROVISIONING_MIN_SHARE_SIZE]:
|
||||
try:
|
||||
common.validate_integer(value, 'share_size', min_value=1)
|
||||
return True
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
return False
|
||||
|
||||
@ -419,3 +430,33 @@ def parse_boolean_extra_spec(extra_spec_key, extra_spec_value):
|
||||
msg = (_('Invalid boolean extra spec %(key)s : %(value)s') %
|
||||
{'key': extra_spec_key, 'value': extra_spec_value})
|
||||
raise exception.InvalidExtraSpec(reason=msg)
|
||||
|
||||
|
||||
def provision_filter_on_size(context, share_type, size):
|
||||
"""This function filters share provisioning requests on size limits.
|
||||
|
||||
If a share type has provisioning size min/max set, this filter
|
||||
will ensure that the share size requested is within the size
|
||||
limits specified in the share type.
|
||||
"""
|
||||
if not share_type:
|
||||
share_type = get_default_share_type()
|
||||
if share_type:
|
||||
size_int = int(size)
|
||||
extra_specs = share_type.get('extra_specs', {})
|
||||
min_size = extra_specs.get(MIN_SIZE_KEY)
|
||||
if min_size and size_int < int(min_size):
|
||||
msg = _("Specified share size of '%(req_size)d' is less "
|
||||
"than the minimum required size of '%(min_size)s' "
|
||||
"for share type '%(sha_type)s'."
|
||||
) % {'req_size': size_int, 'min_size': min_size,
|
||||
'sha_type': share_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 share size of '%(req_size)d' is "
|
||||
"greater than the maximum allowable size of "
|
||||
"'%(max_size)s' for share type '%(sha_type)s'."
|
||||
) % {'req_size': size_int, 'max_size': max_size,
|
||||
'sha_type': share_type['name']}
|
||||
raise exception.InvalidInput(reason=msg)
|
||||
|
@ -120,6 +120,15 @@ class ShareAPITestCase(test.TestCase):
|
||||
self.mock_object(timeutils, 'utcnow',
|
||||
mock.Mock(return_value=self.dt_utc))
|
||||
self.mock_object(share_api.policy, 'check_policy')
|
||||
self._setup_sized_share_types()
|
||||
|
||||
def _setup_sized_share_types(self):
|
||||
"""create a share type with size limit"""
|
||||
spec_dict = {share_types.MIN_SIZE_KEY: 2,
|
||||
share_types.MAX_SIZE_KEY: 4}
|
||||
db_utils.create_share_type(name='limit', extra_specs=spec_dict)
|
||||
self.sized_sha_type = db_api.share_type_get_by_name(self.context,
|
||||
'limit')
|
||||
|
||||
def _setup_create_mocks(self, protocol='nfs', **kwargs):
|
||||
share = db_utils.create_share(
|
||||
@ -736,6 +745,24 @@ class ShareAPITestCase(test.TestCase):
|
||||
|
||||
self.assertEqual(expected_az_names, compatible_azs)
|
||||
|
||||
def test_create_share_with_share_type_size_limit(self):
|
||||
self.assertRaises(exception.InvalidInput,
|
||||
self.api.create,
|
||||
self.context,
|
||||
'nfs',
|
||||
1,
|
||||
'display_name',
|
||||
'display_description',
|
||||
share_type=self.sized_sha_type)
|
||||
self.assertRaises(exception.InvalidInput,
|
||||
self.api.create,
|
||||
self.context,
|
||||
'nfs',
|
||||
5,
|
||||
'display_name',
|
||||
'display_description',
|
||||
share_type=self.sized_sha_type)
|
||||
|
||||
@ddt.data(
|
||||
{'availability_zones': None, 'azs_with_subnet': ['fake_az_1']},
|
||||
{'availability_zones': ['fake_az_2'],
|
||||
@ -2794,6 +2821,17 @@ class ShareAPITestCase(test.TestCase):
|
||||
self.assertRaises(exception.InvalidInput,
|
||||
self.api.extend, self.context, share, new_size)
|
||||
|
||||
def test_extend_with_share_type_size_limit(self):
|
||||
share = db_utils.create_share(status=constants.STATUS_AVAILABLE,
|
||||
size=3)
|
||||
self.mock_object(share_types, 'get_share_type',
|
||||
mock.Mock(return_value=self.sized_sha_type))
|
||||
new_size = 5
|
||||
|
||||
self.assertRaises(exception.InvalidInput,
|
||||
self.api.extend, self.context,
|
||||
share, new_size)
|
||||
|
||||
def _setup_extend_mocks(self, supports_replication):
|
||||
replica_list = []
|
||||
if supports_replication:
|
||||
@ -2933,6 +2971,17 @@ class ShareAPITestCase(test.TestCase):
|
||||
self.context, share, new_size
|
||||
)
|
||||
|
||||
def test_shrink_with_share_type_size_limit(self):
|
||||
share = db_utils.create_share(status=constants.STATUS_AVAILABLE,
|
||||
size=3)
|
||||
self.mock_object(share_types, 'get_share_type',
|
||||
mock.Mock(return_value=self.sized_sha_type))
|
||||
new_size = 1
|
||||
|
||||
self.assertRaises(exception.InvalidInput,
|
||||
self.api.shrink, self.context,
|
||||
share, new_size)
|
||||
|
||||
def test_snapshot_allow_access(self):
|
||||
access_to = '1.1.1.1'
|
||||
access_type = 'ip'
|
||||
@ -3178,6 +3227,19 @@ class ShareAPITestCase(test.TestCase):
|
||||
self.context, share.instance['id'],
|
||||
{'status': constants.STATUS_MIGRATING})
|
||||
|
||||
def test_migration_start_with_new_share_type_limit(self):
|
||||
host = 'fake2@backend#pool'
|
||||
self.mock_object(utils, 'validate_service_host')
|
||||
share = db_utils.create_share(
|
||||
status=constants.STATUS_AVAILABLE,
|
||||
size=1)
|
||||
self.assertRaises(exception.InvalidInput,
|
||||
self.api.migration_start,
|
||||
self.context,
|
||||
share, host, False, True,
|
||||
True, True, True, None,
|
||||
self.sized_sha_type)
|
||||
|
||||
def test_migration_start_destination_az_unsupported(self):
|
||||
host = 'fake2@backend#pool'
|
||||
host_without_pool = host.split('#')[0]
|
||||
|
@ -2616,6 +2616,8 @@ class ShareManagerTestCase(test.TestCase):
|
||||
self.share_manager, '_get_extra_specs_from_share_type',
|
||||
mock.Mock(return_value={}))
|
||||
self.mock_object(self.share_manager.db, 'share_update', mock.Mock())
|
||||
self.mock_object(share_types, 'get_share_type', mock.Mock())
|
||||
self.mock_object(share_types, 'provision_filter_on_size', mock.Mock())
|
||||
share = db_utils.create_share()
|
||||
share_id = share['id']
|
||||
driver_options = {'fake': 'fake'}
|
||||
@ -2722,6 +2724,8 @@ class ShareManagerTestCase(test.TestCase):
|
||||
"manage_existing",
|
||||
mock.Mock(return_value=None))
|
||||
self.mock_object(self.share_manager.db, 'share_update', mock.Mock())
|
||||
self.mock_object(share_types, 'get_share_type', mock.Mock())
|
||||
self.mock_object(share_types, 'provision_filter_on_size', mock.Mock())
|
||||
self.mock_object(
|
||||
self.share_manager, '_get_extra_specs_from_share_type',
|
||||
mock.Mock(return_value={}))
|
||||
@ -2755,6 +2759,8 @@ class ShareManagerTestCase(test.TestCase):
|
||||
self.mock_object(quota.QUOTAS, 'reserve',
|
||||
mock.Mock(side_effect=exception.QuotaError))
|
||||
self.mock_object(self.share_manager.db, 'share_update', mock.Mock())
|
||||
self.mock_object(share_types, 'get_share_type', mock.Mock())
|
||||
self.mock_object(share_types, 'provision_filter_on_size', mock.Mock())
|
||||
self.mock_object(
|
||||
self.share_manager, '_get_extra_specs_from_share_type',
|
||||
mock.Mock(return_value={}))
|
||||
@ -2780,6 +2786,8 @@ class ShareManagerTestCase(test.TestCase):
|
||||
self.mock_object(self.share_manager, 'driver')
|
||||
self.share_manager.driver.driver_handles_share_servers = False
|
||||
share = db_utils.create_share()
|
||||
self.mock_object(share_types, 'get_share_type', mock.Mock())
|
||||
self.mock_object(share_types, 'provision_filter_on_size', mock.Mock())
|
||||
self.mock_object(share_types,
|
||||
'get_share_type_extra_specs',
|
||||
mock.Mock(return_value="True"))
|
||||
@ -2813,6 +2821,8 @@ class ShareManagerTestCase(test.TestCase):
|
||||
export_locations = driver_data.get('export_locations')
|
||||
self.mock_object(self.share_manager.db, 'share_update', mock.Mock())
|
||||
self.mock_object(quota.QUOTAS, 'reserve', mock.Mock())
|
||||
self.mock_object(share_types, 'get_share_type', mock.Mock())
|
||||
self.mock_object(share_types, 'provision_filter_on_size', mock.Mock())
|
||||
self.mock_object(
|
||||
self.share_manager.db,
|
||||
'share_export_locations_update',
|
||||
|
@ -526,3 +526,58 @@ class ShareTypesTestCase(test.TestCase):
|
||||
share_types.parse_boolean_extra_spec,
|
||||
'fake_key',
|
||||
spec_value)
|
||||
|
||||
def test_provision_filter_on_size(self):
|
||||
share_types.create(self.context, "type1",
|
||||
extra_specs={
|
||||
"key1": "val1",
|
||||
"key2": "val2",
|
||||
"driver_handles_share_servers": False})
|
||||
share_types.create(self.context, "type2",
|
||||
extra_specs={
|
||||
share_types.MIN_SIZE_KEY: "12",
|
||||
"key3": "val3",
|
||||
"driver_handles_share_servers": False})
|
||||
share_types.create(self.context, "type3",
|
||||
extra_specs={
|
||||
share_types.MAX_SIZE_KEY: "99",
|
||||
"key4": "val4",
|
||||
"driver_handles_share_servers": False})
|
||||
share_types.create(self.context, "type4",
|
||||
extra_specs={
|
||||
share_types.MIN_SIZE_KEY: "24",
|
||||
share_types.MAX_SIZE_KEY: "99",
|
||||
"key4": "val4",
|
||||
"driver_handles_share_servers": False})
|
||||
|
||||
# Make sure we don't raise if there are no min/max set
|
||||
type1 = share_types.get_share_type_by_name(self.context, 'type1')
|
||||
share_types.provision_filter_on_size(self.context, type1, "11")
|
||||
|
||||
# verify minimum size requirements
|
||||
type2 = share_types.get_share_type_by_name(self.context, 'type2')
|
||||
self.assertRaises(exception.InvalidInput,
|
||||
share_types.provision_filter_on_size,
|
||||
self.context, type2, "11")
|
||||
share_types.provision_filter_on_size(self.context, type2, "12")
|
||||
share_types.provision_filter_on_size(self.context, type2, "100")
|
||||
|
||||
# verify max size requirements
|
||||
type3 = share_types.get_share_type_by_name(self.context, 'type3')
|
||||
self.assertRaises(exception.InvalidInput,
|
||||
share_types.provision_filter_on_size,
|
||||
self.context, type3, "100")
|
||||
share_types.provision_filter_on_size(self.context, type3, "99")
|
||||
share_types.provision_filter_on_size(self.context, type3, "1")
|
||||
|
||||
# verify min and max
|
||||
type4 = share_types.get_share_type_by_name(self.context, 'type4')
|
||||
self.assertRaises(exception.InvalidInput,
|
||||
share_types.provision_filter_on_size,
|
||||
self.context, type4, "20")
|
||||
self.assertRaises(exception.InvalidInput,
|
||||
share_types.provision_filter_on_size,
|
||||
self.context, type4, "100")
|
||||
share_types.provision_filter_on_size(self.context, type4, "24")
|
||||
share_types.provision_filter_on_size(self.context, type4, "99")
|
||||
share_types.provision_filter_on_size(self.context, type4, "30")
|
||||
|
@ -0,0 +1,5 @@
|
||||
---
|
||||
features:
|
||||
- Ability to add minimum and maximum share size restrictions which
|
||||
can be set on a per share-type granularity. Added new extra specs
|
||||
'provisioning:max_share_size' and 'provisioning:min_share_size'.
|
Loading…
x
Reference in New Issue
Block a user