Add config option to set per_share_size_limit.
This feature allows admin to set share size limit for a project. The defaults will either come from the default values set in the quota configuration option or via manila.conf if the user has configured default values for quotas there. The quota_per_share_gigabytes defaults to -1["No Limit"] always unless changed in manila.conf by admin. Closes-Bug: #1811943 Change-Id: Ida126c8c419b8bf4d2a194f061a0809d52b47ab8
This commit is contained in:
parent
aa298c9a8c
commit
0045293942
@ -164,13 +164,14 @@ REST_API_VERSION_HISTORY = """
|
||||
provisioning:min_share_size extra specs,
|
||||
which can add minimum and maximum share size restrictions
|
||||
on a per share-type granularity.
|
||||
* 2.62 - Added quota control to per share size.
|
||||
"""
|
||||
|
||||
# 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.61"
|
||||
_MAX_API_VERSION = "2.62"
|
||||
DEFAULT_API_VERSION = _MIN_API_VERSION
|
||||
|
||||
|
||||
|
@ -337,3 +337,7 @@ user documentation.
|
||||
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'.
|
||||
|
||||
2.62
|
||||
----
|
||||
Added quota control to per share size.
|
||||
|
@ -329,6 +329,9 @@ class QuotaSetsController(QuotaSetsMixin, wsgi.Controller):
|
||||
elif req.api_version_request < api_version.APIVersionRequest("2.53"):
|
||||
self._ensure_specific_microversion_args_are_absent(
|
||||
body, ['share_replicas', 'replica_gigabytes'], "2.53")
|
||||
elif req.api_version_request < api_version.APIVersionRequest("2.62"):
|
||||
self._ensure_specific_microversion_args_are_absent(
|
||||
body, ['per_share_gigabytes'], "2.62")
|
||||
return self._update(req, id, body)
|
||||
|
||||
@wsgi.Controller.api_version('2.7')
|
||||
|
@ -22,6 +22,7 @@ class ViewBuilder(common.ViewBuilder):
|
||||
_detail_version_modifiers = [
|
||||
"add_share_group_quotas",
|
||||
"add_share_replica_quotas",
|
||||
"add_per_share_gigabytes_quotas",
|
||||
]
|
||||
|
||||
def detail_list(self, request, quota_class_set, quota_class=None):
|
||||
@ -52,3 +53,8 @@ class ViewBuilder(common.ViewBuilder):
|
||||
def add_share_replica_quotas(self, context, view, quota_class_set):
|
||||
view['share_replicas'] = quota_class_set.get('share_replicas')
|
||||
view['replica_gigabytes'] = quota_class_set.get('replica_gigabytes')
|
||||
|
||||
@common.ViewBuilder.versioned_method("2.62")
|
||||
def add_per_share_gigabytes_quotas(self, context, view, quota_class_set):
|
||||
view['per_share_gigabytes'] = quota_class_set.get(
|
||||
'per_share_gigabytes')
|
||||
|
@ -22,6 +22,7 @@ class ViewBuilder(common.ViewBuilder):
|
||||
_detail_version_modifiers = [
|
||||
"add_share_group_quotas",
|
||||
"add_share_replica_quotas",
|
||||
"add_per_share_gigabytes_quotas",
|
||||
]
|
||||
|
||||
def detail_list(self, request, quota_set, project_id=None,
|
||||
@ -59,3 +60,7 @@ class ViewBuilder(common.ViewBuilder):
|
||||
def add_share_replica_quotas(self, context, view, quota_class_set):
|
||||
view['share_replicas'] = quota_class_set.get('share_replicas')
|
||||
view['replica_gigabytes'] = quota_class_set.get('replica_gigabytes')
|
||||
|
||||
@common.ViewBuilder.versioned_method("2.62")
|
||||
def add_per_share_gigabytes_quotas(self, context, view, quota_set):
|
||||
view['per_share_gigabytes'] = quota_set.get('per_share_gigabytes')
|
||||
|
@ -0,0 +1,61 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
"""add_per_share_gigabytes_quota_class
|
||||
|
||||
Revision ID: 0c23aec99b74
|
||||
Revises: 5aa813ae673d
|
||||
Create Date: 2021-01-03 10:01:57.276225
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '0c23aec99b74'
|
||||
down_revision = '5aa813ae673d'
|
||||
|
||||
from alembic import op
|
||||
from manila.db.migrations import utils
|
||||
from oslo_log import log
|
||||
from oslo_utils import timeutils
|
||||
from sqlalchemy import MetaData
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
|
||||
def upgrade():
|
||||
meta = MetaData()
|
||||
meta.bind = op.get_bind()
|
||||
connection = op.get_bind().connect()
|
||||
quota_classes_table = utils.load_table('quota_classes', connection)
|
||||
|
||||
try:
|
||||
op.bulk_insert
|
||||
(quota_classes_table,
|
||||
[{'created_at': timeutils.utcnow(),
|
||||
'class_name': 'default',
|
||||
'resource': 'per_share_gigabytes',
|
||||
'hard_limit': -1,
|
||||
'deleted': False, }])
|
||||
except Exception:
|
||||
LOG.error("Default per_share_gigabytes row not inserted "
|
||||
"into the quota_classes.")
|
||||
raise
|
||||
|
||||
|
||||
def downgrade():
|
||||
"""Don't delete the 'default' entries at downgrade time.
|
||||
|
||||
We don't know if the user had default entries when we started.
|
||||
If they did, we wouldn't want to remove them. So, the safest
|
||||
thing to do is just leave the 'default' entries at downgrade time.
|
||||
"""
|
||||
pass
|
@ -424,6 +424,12 @@ class SnapshotSizeExceedsAvailableQuota(QuotaError):
|
||||
"gigabytes quota.")
|
||||
|
||||
|
||||
class ShareSizeExceedsLimit(QuotaError):
|
||||
message = _(
|
||||
"Requested share size %(size)d is larger than "
|
||||
"maximum allowed limit %(limit)d.")
|
||||
|
||||
|
||||
class ShareLimitExceeded(QuotaError):
|
||||
message = _(
|
||||
"Maximum number of shares allowed (%(allowed)d) either per "
|
||||
|
@ -39,6 +39,9 @@ quota_opts = [
|
||||
cfg.IntOpt('quota_gigabytes',
|
||||
default=1000,
|
||||
help='Number of share gigabytes allowed per project.'),
|
||||
cfg.IntOpt('quota_per_share_gigabytes',
|
||||
default=-1,
|
||||
help='Max size allowed per share, in gigabytes.'),
|
||||
cfg.IntOpt('quota_snapshot_gigabytes',
|
||||
default=1000,
|
||||
help='Number of snapshot gigabytes allowed per project.'),
|
||||
@ -383,6 +386,50 @@ class DbQuotaDriver(object):
|
||||
|
||||
return {k: v['limit'] for k, v in quotas.items()}
|
||||
|
||||
def limit_check(self, context, resources, values, project_id=None):
|
||||
"""Check simple quota limits.
|
||||
|
||||
For limits--those quotas for which there is no usage
|
||||
synchronization function--this method checks that a set of
|
||||
proposed values are permitted by the limit restriction.
|
||||
|
||||
This method will raise a QuotaResourceUnknown exception if a
|
||||
given resource is unknown or if it is not a simple limit
|
||||
resource.
|
||||
|
||||
If any of the proposed values is over the defined quota, an
|
||||
OverQuota exception will be raised with the sorted list of the
|
||||
resources which are too high. Otherwise, the method returns
|
||||
nothing.
|
||||
|
||||
:param context: The request context, for access checks.
|
||||
:param resources: A dictionary of the registered resources.
|
||||
:param values: A dictionary of the values to check against the
|
||||
quota.
|
||||
:param project_id: Specify the project_id if current context
|
||||
is admin and admin wants to impact on
|
||||
common user's tenant.
|
||||
"""
|
||||
|
||||
# Ensure no value is less than zero
|
||||
unders = [key for key, val in values.items() if val < 0]
|
||||
if unders:
|
||||
raise exception.InvalidQuotaValue(unders=sorted(unders))
|
||||
|
||||
# If project_id is None, then we use the project_id in context
|
||||
if project_id is None:
|
||||
project_id = context.project_id
|
||||
|
||||
quotas = self._get_quotas(context, resources, values.keys(),
|
||||
has_sync=False, project_id=project_id)
|
||||
# Check the quotas and construct a list of the resources that
|
||||
# would be put over limit by the desired values
|
||||
overs = [key for key, val in values.items()
|
||||
if quotas[key] >= 0 and quotas[key] < val]
|
||||
if overs:
|
||||
raise exception.OverQuota(overs=sorted(overs), quotas=quotas,
|
||||
usages={})
|
||||
|
||||
def reserve(self, context, resources, deltas, expire=None,
|
||||
project_id=None, user_id=None, share_type_id=None,
|
||||
overquota_allowed=False):
|
||||
@ -657,7 +704,8 @@ class ReservableResource(BaseResource):
|
||||
"""
|
||||
|
||||
super(ReservableResource, self).__init__(name, flag=flag)
|
||||
self.sync = sync
|
||||
if sync:
|
||||
self.sync = sync
|
||||
|
||||
|
||||
class AbsoluteResource(BaseResource):
|
||||
@ -876,6 +924,34 @@ class QuotaEngine(object):
|
||||
|
||||
return res.count(context, *args, **kwargs)
|
||||
|
||||
def limit_check(self, context, project_id=None, **values):
|
||||
"""Check simple quota limits.
|
||||
|
||||
For limits--those quotas for which there is no usage
|
||||
synchronization function--this method checks that a set of
|
||||
proposed values are permitted by the limit restriction. The
|
||||
values to check are given as keyword arguments, where the key
|
||||
identifies the specific quota limit to check, and the value is
|
||||
the proposed value.
|
||||
|
||||
This method will raise a QuotaResourceUnknown exception if a
|
||||
given resource is unknown or if it is not a simple limit
|
||||
resource.
|
||||
|
||||
If any of the proposed values is over the defined quota, an
|
||||
OverQuota exception will be raised with the sorted list of the
|
||||
resources which are too high. Otherwise, the method returns
|
||||
nothing.
|
||||
|
||||
:param context: The request context, for access checks.
|
||||
:param project_id: Specify the project_id if current context
|
||||
is admin and admin wants to impact on
|
||||
common user's tenant.
|
||||
"""
|
||||
|
||||
return self._driver.limit_check(context, self._resources, values,
|
||||
project_id=project_id)
|
||||
|
||||
def reserve(self, context, expire=None, project_id=None, user_id=None,
|
||||
share_type_id=None, overquota_allowed=False, **deltas):
|
||||
"""Check quotas and reserve resources.
|
||||
@ -1059,6 +1135,8 @@ resources = [
|
||||
ReservableResource('shares', '_sync_shares', 'quota_shares'),
|
||||
ReservableResource('snapshots', '_sync_snapshots', 'quota_snapshots'),
|
||||
ReservableResource('gigabytes', '_sync_gigabytes', 'quota_gigabytes'),
|
||||
ReservableResource('per_share_gigabytes', None,
|
||||
'quota_per_share_gigabytes'),
|
||||
ReservableResource('snapshot_gigabytes', '_sync_snapshot_gigabytes',
|
||||
'quota_snapshot_gigabytes'),
|
||||
ReservableResource('share_networks', '_sync_share_networks',
|
||||
|
@ -236,6 +236,8 @@ class API(base.Base):
|
||||
supported=CONF.enabled_share_protocols))
|
||||
raise exception.InvalidInput(reason=msg)
|
||||
|
||||
self._check_is_share_size_within_per_share_quota_limit(context, size)
|
||||
|
||||
deltas = {'shares': 1, 'gigabytes': size}
|
||||
share_type_attributes = self.get_share_attributes_from_share_type(
|
||||
share_type)
|
||||
@ -2020,6 +2022,17 @@ class API(base.Base):
|
||||
}
|
||||
raise exception.ShareBusyException(reason=msg)
|
||||
|
||||
def _check_is_share_size_within_per_share_quota_limit(self, context, size):
|
||||
"""Raises an exception if share size above per share quota limit."""
|
||||
try:
|
||||
values = {'per_share_gigabytes': size}
|
||||
QUOTAS.limit_check(context, project_id=context.project_id,
|
||||
**values)
|
||||
except exception.OverQuota as e:
|
||||
quotas = e.kwargs['quotas']
|
||||
raise exception.ShareSizeExceedsLimit(
|
||||
size=size, limit=quotas['per_share_gigabytes'])
|
||||
|
||||
def _check_metadata_properties(self, metadata=None):
|
||||
if not metadata:
|
||||
metadata = {}
|
||||
@ -2098,6 +2111,9 @@ class API(base.Base):
|
||||
'size': share['size']})
|
||||
raise exception.InvalidInput(reason=msg)
|
||||
|
||||
self._check_is_share_size_within_per_share_quota_limit(context,
|
||||
new_size)
|
||||
|
||||
# ensure we pass the share_type provisioning filter on size
|
||||
try:
|
||||
share_type = share_types.get_share_type(
|
||||
|
@ -2601,6 +2601,16 @@ class ShareManager(manager.SchedulerDependentManager):
|
||||
share_types.provision_filter_on_size(context,
|
||||
share_type,
|
||||
share_update.get('size'))
|
||||
try:
|
||||
values = {'per_share_gigabytes': share_update.get('size')}
|
||||
QUOTAS.limit_check(context, project_id=context.project_id,
|
||||
**values)
|
||||
except exception.OverQuota as e:
|
||||
quotas = e.kwargs['quotas']
|
||||
LOG.warning("Requested share size %(size)d is larger than "
|
||||
"maximum allowed limit %(limit)d.",
|
||||
{'size': share_update.get('size'),
|
||||
'limit': quotas['per_share_gigabytes']})
|
||||
|
||||
deltas = {
|
||||
'project_id': project_id,
|
||||
|
@ -62,6 +62,7 @@ class QuotaSetsControllerTest(test.TestCase):
|
||||
('os-', '2.6', quota_class_sets.QuotaClassSetsControllerLegacy),
|
||||
('', '2.7', quota_class_sets.QuotaClassSetsController),
|
||||
('', '2.53', quota_class_sets.QuotaClassSetsController),
|
||||
('', '2.62', quota_class_sets.QuotaClassSetsController),
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_show_quota(self, url, version, controller):
|
||||
@ -94,6 +95,8 @@ class QuotaSetsControllerTest(test.TestCase):
|
||||
if req.api_version_request >= api_version.APIVersionRequest("2.53"):
|
||||
expected['quota_class_set']['share_replicas'] = 100
|
||||
expected['quota_class_set']['replica_gigabytes'] = 1000
|
||||
if req.api_version_request >= api_version.APIVersionRequest("2.62"):
|
||||
expected['quota_class_set']['per_share_gigabytes'] = -1
|
||||
|
||||
result = controller().show(req, self.class_name)
|
||||
|
||||
@ -119,6 +122,7 @@ class QuotaSetsControllerTest(test.TestCase):
|
||||
('os-', '2.6', quota_class_sets.QuotaClassSetsControllerLegacy),
|
||||
('', '2.7', quota_class_sets.QuotaClassSetsController),
|
||||
('', '2.53', quota_class_sets.QuotaClassSetsController),
|
||||
('', '2.62', quota_class_sets.QuotaClassSetsController),
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_update_quota(self, url, version, controller):
|
||||
@ -148,6 +152,8 @@ class QuotaSetsControllerTest(test.TestCase):
|
||||
if req.api_version_request >= api_version.APIVersionRequest("2.53"):
|
||||
expected['quota_class_set']['share_replicas'] = 100
|
||||
expected['quota_class_set']['replica_gigabytes'] = 1000
|
||||
if req.api_version_request >= api_version.APIVersionRequest("2.62"):
|
||||
expected['quota_class_set']['per_share_gigabytes'] = -1
|
||||
|
||||
update_result = controller().update(
|
||||
req, self.class_name, body=body)
|
||||
|
@ -37,6 +37,7 @@ from manila import utils
|
||||
CONF = cfg.CONF
|
||||
sg_quota_keys = ['share_groups', 'share_group_snapshots']
|
||||
replica_quota_keys = ['share_replicas']
|
||||
per_share_size_quota_keys = ['per_share_gigabytes']
|
||||
|
||||
|
||||
def _get_request(is_admin, user_in_url):
|
||||
@ -172,7 +173,6 @@ class QuotaSetsControllerTest(test.TestCase):
|
||||
'reserved': 0,
|
||||
},
|
||||
}}
|
||||
|
||||
for k, v in quotas.items():
|
||||
CONF.set_default('quota_' + k, v)
|
||||
|
||||
@ -277,6 +277,7 @@ class QuotaSetsControllerTest(test.TestCase):
|
||||
({"quota_set": {"foo": "bar"}}, sg_quota_keys, '2.40'),
|
||||
({"foo": "bar"}, replica_quota_keys, '2.53'),
|
||||
({"quota_set": {"foo": "bar"}}, replica_quota_keys, '2.53'),
|
||||
({"quota_set": {"foo": "bar"}}, per_share_size_quota_keys, '2.62'),
|
||||
)
|
||||
@ddt.unpack
|
||||
def test__ensure_specific_microversion_args_are_absent_success(
|
||||
@ -293,6 +294,8 @@ class QuotaSetsControllerTest(test.TestCase):
|
||||
({"quota_set": {"share_group_snapshots": 8}}, sg_quota_keys, '2.40'),
|
||||
({"quota_set": {"share_replicas": 9}}, replica_quota_keys, '2.53'),
|
||||
({"quota_set": {"share_replicas": 10}}, replica_quota_keys, '2.53'),
|
||||
({"quota_set": {"per_share_gigabytes": 10}},
|
||||
per_share_size_quota_keys, '2.62'),
|
||||
)
|
||||
@ddt.unpack
|
||||
def test__ensure_specific_microversion_args_are_absent_error(
|
||||
@ -351,7 +354,6 @@ class QuotaSetsControllerTest(test.TestCase):
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
for k, v in quotas.items():
|
||||
CONF.set_default('quota_' + k, v)
|
||||
|
||||
|
@ -35,6 +35,7 @@ class ViewBuilderTestCase(test.TestCase):
|
||||
("fake_quota_class", "2.40"), (None, "2.40"),
|
||||
("fake_quota_class", "2.39"), (None, "2.39"),
|
||||
("fake_quota_class", "2.53"), (None, "2.53"),
|
||||
("fake_quota_class", "2.62"), (None, "2.62"),
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_detail_list_with_share_type(self, quota_class, microversion):
|
||||
@ -75,6 +76,12 @@ class ViewBuilderTestCase(test.TestCase):
|
||||
quota_class_set['share_replicas'] = fake_share_replicas_value
|
||||
quota_class_set['replica_gigabytes'] = fake_replica_gigabytes_value
|
||||
|
||||
if req.api_version_request >= api_version.APIVersionRequest("2.62"):
|
||||
fake_per_share_gigabytes = 10
|
||||
expected[self.builder._collection_name][
|
||||
"per_share_gigabytes"] = fake_per_share_gigabytes
|
||||
quota_class_set['per_share_gigabytes'] = fake_per_share_gigabytes
|
||||
|
||||
result = self.builder.detail_list(
|
||||
req, quota_class_set, quota_class=quota_class)
|
||||
|
||||
|
@ -43,6 +43,9 @@ class ViewBuilderTestCase(test.TestCase):
|
||||
(None, 'fake_share_type_id', "2.53"),
|
||||
('fake_project_id', None, "2.53"),
|
||||
(None, None, "2.53"),
|
||||
(None, 'fake_share_type_id', "2.62"),
|
||||
('fake_project_id', None, "2.62"),
|
||||
(None, None, "2.62"),
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_detail_list_with_share_type(self, project_id, share_type,
|
||||
@ -86,6 +89,12 @@ class ViewBuilderTestCase(test.TestCase):
|
||||
quota_set['share_replicas'] = fake_share_replicas_value
|
||||
quota_set['replica_gigabytes'] = fake_replica_gigabytes_value
|
||||
|
||||
if req.api_version_request >= api_version.APIVersionRequest("2.62"):
|
||||
fake_per_share_gigabytes = 10
|
||||
expected[self.builder._collection_name]["per_share_gigabytes"] = (
|
||||
fake_per_share_gigabytes)
|
||||
quota_set['per_share_gigabytes'] = fake_per_share_gigabytes
|
||||
|
||||
result = self.builder.detail_list(
|
||||
req, quota_set, project_id=project_id, share_type=share_type)
|
||||
|
||||
|
@ -889,6 +889,31 @@ class ShareAPITestCase(test.TestCase):
|
||||
self.context, share_type_id=None,
|
||||
shares=1, gigabytes=share_data['size'])
|
||||
|
||||
@ddt.data({'overs': {'per_share_gigabytes': 'fake'},
|
||||
'expected_exception': exception.ShareSizeExceedsLimit})
|
||||
@ddt.unpack
|
||||
def test_create_share_over_per_share_quota(self, overs,
|
||||
expected_exception):
|
||||
share, share_data = self._setup_create_mocks()
|
||||
|
||||
quota.CONF.set_default("quota_per_share_gigabytes", 5)
|
||||
share_data['size'] = 20
|
||||
|
||||
usages = {'per_share_gigabytes': {'reserved': 0, 'in_use': 0}}
|
||||
quotas = {'per_share_gigabytes': 10}
|
||||
exc = exception.OverQuota(overs=overs, usages=usages, quotas=quotas)
|
||||
self.mock_object(quota.QUOTAS, 'reserve', mock.Mock(side_effect=exc))
|
||||
|
||||
self.assertRaises(
|
||||
expected_exception,
|
||||
self.api.create,
|
||||
self.context,
|
||||
share_data['share_proto'],
|
||||
share_data['size'],
|
||||
share_data['display_name'],
|
||||
share_data['display_description']
|
||||
)
|
||||
|
||||
@ddt.data(exception.QuotaError, exception.InvalidShare)
|
||||
def test_create_share_error_on_quota_commit(self, expected_exception):
|
||||
share, share_data = self._setup_create_mocks()
|
||||
@ -2823,6 +2848,14 @@ class ShareAPITestCase(test.TestCase):
|
||||
self.assertRaises(exception.InvalidInput,
|
||||
self.api.extend, self.context, share, new_size)
|
||||
|
||||
def test_extend_share_over_per_share_quota(self):
|
||||
quota.CONF.set_default("quota_per_share_gigabytes", 5)
|
||||
share = db_utils.create_share(status=constants.STATUS_AVAILABLE,
|
||||
size=4)
|
||||
new_size = 6
|
||||
self.assertRaises(exception.ShareSizeExceedsLimit,
|
||||
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)
|
||||
|
@ -602,3 +602,10 @@ class ManilaExceptionResponseCode413(test.TestCase):
|
||||
# verify response code for exception.PortLimitExceeded
|
||||
e = exception.PortLimitExceeded()
|
||||
self.assertEqual(413, e.code)
|
||||
|
||||
def test_per_share_limit_exceeded(self):
|
||||
# verify response code for exception.ShareSizeExceedsLimit
|
||||
size = 779 # amount of share size
|
||||
limit = 775 # amount of allowed share size limit
|
||||
e = exception.ShareSizeExceedsLimit(size=size, limit=limit)
|
||||
self.assertEqual(413, e.code)
|
||||
|
@ -714,7 +714,7 @@ class QuotaEngineTestCase(test.TestCase):
|
||||
|
||||
def test_current_common_resources(self):
|
||||
self.assertEqual(
|
||||
['gigabytes', 'replica_gigabytes', 'share_group_snapshots',
|
||||
'share_groups', 'share_networks', 'share_replicas', 'shares',
|
||||
'snapshot_gigabytes', 'snapshots'],
|
||||
['gigabytes', 'per_share_gigabytes', 'replica_gigabytes',
|
||||
'share_group_snapshots', 'share_groups', 'share_networks',
|
||||
'share_replicas', 'shares', 'snapshot_gigabytes', 'snapshots'],
|
||||
quota.QUOTAS.resources)
|
||||
|
@ -0,0 +1,6 @@
|
||||
---
|
||||
features:
|
||||
- |
|
||||
'quota_per_share_gigabytes' config option allows admin to set per share
|
||||
size limit for a project. The default value is -1["No Limit"] always
|
||||
unless changed in manila.conf by admin.
|
Loading…
Reference in New Issue
Block a user