Add quotas per share type

With this feature it will be possible to set quotas per share type
for all existing quota resources. It is useful for deployments with
multiple backends that are accessible via different share types.

Also, fix one of existing DB migrations that hangs on PostgreSQL.

APIImpact
DocImpact
Implements blueprint support-quotas-per-share-type
Change-Id: I8472418c2eb363cf5a76c672c7fdea72f21e4f63
This commit is contained in:
Valeriy Ponomaryov 2017-03-27 15:44:17 +03:00
parent b61f46452c
commit 05c42ecf70
24 changed files with 2278 additions and 2011 deletions

View File

@ -109,14 +109,14 @@ REST_API_VERSION_HISTORY = """
* 2.37 - Added /messages APIs. * 2.37 - Added /messages APIs.
* 2.38 - Support IPv6 validation in allow_access API to enable IPv6 in * 2.38 - Support IPv6 validation in allow_access API to enable IPv6 in
manila. manila.
* 2.39 - Added share-type quotas.
""" """
# The minimum and maximum versions of the API supported # The minimum and maximum versions of the API supported
# The default api version request is defined to be the # The default api version request is defined to be the
# minimum version of the API supported. # minimum version of the API supported.
_MIN_API_VERSION = "2.0" _MIN_API_VERSION = "2.0"
_MAX_API_VERSION = "2.38" _MAX_API_VERSION = "2.39"
DEFAULT_API_VERSION = _MIN_API_VERSION DEFAULT_API_VERSION = _MIN_API_VERSION

View File

@ -218,3 +218,7 @@ user documentation.
2.38 2.38
---- ----
Support IPv6 format validation in allow_access API to enable IPv6. Support IPv6 format validation in allow_access API to enable IPv6.
2.39
----
Added share-type quotas.

View File

@ -19,6 +19,7 @@ from oslo_utils import strutils
from six.moves.urllib import parse from six.moves.urllib import parse
import webob import webob
from manila.api.openstack import api_version_request as api_version
from manila.api.openstack import wsgi from manila.api.openstack import wsgi
from manila.api.views import quota_sets as quota_sets_views from manila.api.views import quota_sets as quota_sets_views
from manila import db from manila import db
@ -28,7 +29,7 @@ from manila import quota
QUOTAS = quota.QUOTAS QUOTAS = quota.QUOTAS
LOG = log.getLogger(__name__) LOG = log.getLogger(__name__)
NON_QUOTA_KEYS = ('tenant_id', 'id', 'force') NON_QUOTA_KEYS = ('tenant_id', 'id', 'force', 'share_type')
class QuotaSetsMixin(object): class QuotaSetsMixin(object):
@ -41,7 +42,8 @@ class QuotaSetsMixin(object):
resource_name = "quota_set" resource_name = "quota_set"
_view_builder_class = quota_sets_views.ViewBuilder _view_builder_class = quota_sets_views.ViewBuilder
def _validate_quota_limit(self, limit, minimum, maximum, force_update): @staticmethod
def _validate_quota_limit(limit, minimum, maximum, force_update):
# NOTE: -1 is a flag value for unlimited # NOTE: -1 is a flag value for unlimited
if limit < -1: if limit < -1:
msg = _("Quota limit must be -1 or greater.") msg = _("Quota limit must be -1 or greater.")
@ -50,17 +52,49 @@ class QuotaSetsMixin(object):
(maximum != -1 or (maximum == -1 and limit != -1))): (maximum != -1 or (maximum == -1 and limit != -1))):
msg = _("Quota limit must be greater than %s.") % minimum msg = _("Quota limit must be greater than %s.") % minimum
raise webob.exc.HTTPBadRequest(explanation=msg) raise webob.exc.HTTPBadRequest(explanation=msg)
if maximum != -1 and limit > maximum: if maximum != -1 and limit > maximum and not force_update:
msg = _("Quota limit must be less than %s.") % maximum msg = _("Quota limit must be less than %s.") % maximum
raise webob.exc.HTTPBadRequest(explanation=msg) raise webob.exc.HTTPBadRequest(explanation=msg)
def _get_quotas(self, context, id, user_id=None, usages=False): @staticmethod
if user_id: def _validate_user_id_and_share_type_args(user_id, share_type):
values = QUOTAS.get_user_quotas(context, id, user_id, if user_id and share_type:
usages=usages) msg = _("'user_id' and 'share_type' values are mutually exclusive")
else: raise webob.exc.HTTPBadRequest(explanation=msg)
values = QUOTAS.get_project_quotas(context, id, usages=usages)
@staticmethod
def _get_share_type_id(context, share_type_name_or_id):
if share_type_name_or_id:
share_type = db.share_type_get_by_name_or_id(
context, share_type_name_or_id)
if share_type:
return share_type['id']
msg = _("Share type with name or id '%s' not found.") % (
share_type_name_or_id)
raise webob.exc.HTTPNotFound(explanation=msg)
@staticmethod
def _ensure_share_type_arg_is_absent(req):
params = parse.parse_qs(req.environ.get('QUERY_STRING', ''))
share_type = params.get('share_type', [None])[0]
if share_type:
msg = _("'share_type' key is not supported by this microversion. "
"Use 2.38 or greater microversion to be able "
"to use 'share_type' quotas.")
raise webob.exc.HTTPBadRequest(explanation=msg)
def _get_quotas(self, context, project_id, user_id=None,
share_type_id=None, usages=False):
self._validate_user_id_and_share_type_args(user_id, share_type_id)
if user_id:
values = QUOTAS.get_user_quotas(
context, project_id, user_id, usages=usages)
elif share_type_id:
values = QUOTAS.get_share_type_quotas(
context, project_id, share_type_id, usages=usages)
else:
values = QUOTAS.get_project_quotas(
context, project_id, usages=usages)
if usages: if usages:
return values return values
return {k: v['limit'] for k, v in values.items()} return {k: v['limit'] for k, v in values.items()}
@ -70,14 +104,15 @@ class QuotaSetsMixin(object):
context = req.environ['manila.context'] context = req.environ['manila.context']
params = parse.parse_qs(req.environ.get('QUERY_STRING', '')) params = parse.parse_qs(req.environ.get('QUERY_STRING', ''))
user_id = params.get('user_id', [None])[0] user_id = params.get('user_id', [None])[0]
share_type = params.get('share_type', [None])[0]
try: try:
db.authorize_project_context(context, id) db.authorize_project_context(context, id)
# _get_quotas use 'usages' to indicate whether retrieve additional # _get_quotas use 'usages' to indicate whether retrieve additional
# attributes, so pass detail to the argument. # attributes, so pass detail to the argument.
return self._view_builder.detail_list( share_type_id = self._get_share_type_id(context, share_type)
self._get_quotas(context, id, user_id=user_id, quotas = self._get_quotas(
usages=detail), id) context, id, user_id, share_type_id, usages=detail)
return self._view_builder.detail_list(quotas, id, share_type_id)
except exception.NotAuthorized: except exception.NotAuthorized:
raise webob.exc.HTTPForbidden() raise webob.exc.HTTPForbidden()
@ -94,19 +129,25 @@ class QuotaSetsMixin(object):
force_update = False force_update = False
params = parse.parse_qs(req.environ.get('QUERY_STRING', '')) params = parse.parse_qs(req.environ.get('QUERY_STRING', ''))
user_id = params.get('user_id', [None])[0] user_id = params.get('user_id', [None])[0]
share_type = params.get('share_type', [None])[0]
self._validate_user_id_and_share_type_args(user_id, share_type)
share_type_id = self._get_share_type_id(context, share_type)
try: try:
settable_quotas = QUOTAS.get_settable_quotas(context, project_id, settable_quotas = QUOTAS.get_settable_quotas(
user_id=user_id) context, project_id, user_id=user_id,
share_type_id=share_type_id)
except exception.NotAuthorized: except exception.NotAuthorized:
raise webob.exc.HTTPForbidden() raise webob.exc.HTTPForbidden()
for key, value in body.get('quota_set', {}).items(): for key, value in body.get('quota_set', {}).items():
if (key not in QUOTAS and if key == 'share_networks' and share_type_id:
key not in NON_QUOTA_KEYS): msg = _("'share_networks' quota cannot be set for share type. "
"It can be set only for project or user.")
raise webob.exc.HTTPBadRequest(explanation=msg)
elif (key not in QUOTAS and key not in NON_QUOTA_KEYS):
bad_keys.append(key) bad_keys.append(key)
continue elif key == 'force':
if key == 'force':
force_update = strutils.bool_from_string(value) force_update = strutils.bool_from_string(value)
elif key not in NON_QUOTA_KEYS and value: elif key not in NON_QUOTA_KEYS and value:
try: try:
@ -124,8 +165,9 @@ class QuotaSetsMixin(object):
raise webob.exc.HTTPBadRequest(explanation=msg) raise webob.exc.HTTPBadRequest(explanation=msg)
try: try:
quotas = self._get_quotas(context, id, user_id=user_id, quotas = self._get_quotas(
usages=True) context, id, user_id=user_id, share_type_id=share_type_id,
usages=True)
except exception.NotAuthorized: except exception.NotAuthorized:
raise webob.exc.HTTPForbidden() raise webob.exc.HTTPForbidden()
@ -164,25 +206,36 @@ class QuotaSetsMixin(object):
maximum = settable_quotas[key]['maximum'] maximum = settable_quotas[key]['maximum']
self._validate_quota_limit(value, minimum, maximum, force_update) self._validate_quota_limit(value, minimum, maximum, force_update)
try: try:
db.quota_create(context, project_id, key, value, db.quota_create(
user_id=user_id) context, project_id, key, value,
user_id=user_id, share_type_id=share_type_id)
except exception.QuotaExists: except exception.QuotaExists:
db.quota_update(context, project_id, key, value, db.quota_update(
user_id=user_id) context, project_id, key, value,
user_id=user_id, share_type_id=share_type_id)
except exception.AdminRequired: except exception.AdminRequired:
raise webob.exc.HTTPForbidden() raise webob.exc.HTTPForbidden()
return self._view_builder.detail_list( return self._view_builder.detail_list(
self._get_quotas(context, id, user_id=user_id)) self._get_quotas(
context, id, user_id=user_id, share_type_id=share_type_id),
share_type=share_type_id,
)
@wsgi.Controller.authorize("delete") @wsgi.Controller.authorize("delete")
def _delete(self, req, id): def _delete(self, req, id):
context = req.environ['manila.context'] context = req.environ['manila.context']
params = parse.parse_qs(req.environ.get('QUERY_STRING', '')) params = parse.parse_qs(req.environ.get('QUERY_STRING', ''))
user_id = params.get('user_id', [None])[0] user_id = params.get('user_id', [None])[0]
share_type = params.get('share_type', [None])[0]
self._validate_user_id_and_share_type_args(user_id, share_type)
try: try:
db.authorize_project_context(context, id) db.authorize_project_context(context, id)
if user_id: if user_id:
QUOTAS.destroy_all_by_project_and_user(context, id, user_id) QUOTAS.destroy_all_by_project_and_user(context, id, user_id)
elif share_type:
share_type_id = self._get_share_type_id(context, share_type)
QUOTAS.destroy_all_by_project_and_share_type(
context, id, share_type_id)
else: else:
QUOTAS.destroy_all_by_project(context, id) QUOTAS.destroy_all_by_project(context, id)
return webob.Response(status_int=202) return webob.Response(status_int=202)
@ -199,6 +252,7 @@ class QuotaSetsControllerLegacy(QuotaSetsMixin, wsgi.Controller):
@wsgi.Controller.api_version('1.0', '2.6') @wsgi.Controller.api_version('1.0', '2.6')
def show(self, req, id): def show(self, req, id):
self._ensure_share_type_arg_is_absent(req)
return self._show(req, id) return self._show(req, id)
@wsgi.Controller.api_version('1.0', '2.6') @wsgi.Controller.api_version('1.0', '2.6')
@ -207,10 +261,12 @@ class QuotaSetsControllerLegacy(QuotaSetsMixin, wsgi.Controller):
@wsgi.Controller.api_version('1.0', '2.6') @wsgi.Controller.api_version('1.0', '2.6')
def update(self, req, id, body): def update(self, req, id, body):
self._ensure_share_type_arg_is_absent(req)
return self._update(req, id, body) return self._update(req, id, body)
@wsgi.Controller.api_version('1.0', '2.6') @wsgi.Controller.api_version('1.0', '2.6')
def delete(self, req, id): def delete(self, req, id):
self._ensure_share_type_arg_is_absent(req)
return self._delete(req, id) return self._delete(req, id)
@ -223,10 +279,14 @@ class QuotaSetsController(QuotaSetsMixin, wsgi.Controller):
@wsgi.Controller.api_version('2.7') @wsgi.Controller.api_version('2.7')
def show(self, req, id): def show(self, req, id):
if req.api_version_request < api_version.APIVersionRequest("2.39"):
self._ensure_share_type_arg_is_absent(req)
return self._show(req, id) return self._show(req, id)
@wsgi.Controller.api_version('2.25') @wsgi.Controller.api_version('2.25')
def detail(self, req, id): def detail(self, req, id):
if req.api_version_request < api_version.APIVersionRequest("2.39"):
self._ensure_share_type_arg_is_absent(req)
return self._show(req, id, True) return self._show(req, id, True)
@wsgi.Controller.api_version('2.7') @wsgi.Controller.api_version('2.7')
@ -235,10 +295,14 @@ class QuotaSetsController(QuotaSetsMixin, wsgi.Controller):
@wsgi.Controller.api_version('2.7') @wsgi.Controller.api_version('2.7')
def update(self, req, id, body): def update(self, req, id, body):
if req.api_version_request < api_version.APIVersionRequest("2.39"):
self._ensure_share_type_arg_is_absent(req)
return self._update(req, id, body) return self._update(req, id, body)
@wsgi.Controller.api_version('2.7') @wsgi.Controller.api_version('2.7')
def delete(self, req, id): def delete(self, req, id):
if req.api_version_request < api_version.APIVersionRequest("2.39"):
self._ensure_share_type_arg_is_absent(req)
return self._delete(req, id) return self._delete(req, id)

View File

@ -20,16 +20,17 @@ class ViewBuilder(common.ViewBuilder):
_collection_name = "quota_set" _collection_name = "quota_set"
def detail_list(self, quota_set, project_id=None): def detail_list(self, quota_set, project_id=None, share_type=None):
"""Detailed view of quota set.""" """Detailed view of quota set."""
keys = ( keys = (
'shares', 'shares',
'gigabytes', 'gigabytes',
'snapshots', 'snapshots',
'snapshot_gigabytes', 'snapshot_gigabytes',
'share_networks',
) )
view = {key: quota_set.get(key) for key in keys} view = {key: quota_set.get(key) for key in keys}
if project_id: if project_id:
view['id'] = project_id view['id'] = project_id
if not share_type:
view['share_networks'] = quota_set.get('share_networks')
return {self._collection_name: view} return {self._collection_name: view}

View File

@ -136,15 +136,11 @@ def service_update(context, service_id, values):
#################### ####################
def quota_create(context, project_id, resource, limit, user_id=None): def quota_create(context, project_id, resource, limit, user_id=None,
share_type_id=None):
"""Create a quota for the given project and resource.""" """Create a quota for the given project and resource."""
return IMPL.quota_create(context, project_id, resource, limit, return IMPL.quota_create(context, project_id, resource, limit,
user_id=user_id) user_id=user_id, share_type_id=share_type_id)
def quota_get(context, project_id, resource, user_id=None):
"""Retrieve a quota or raise if it does not exist."""
return IMPL.quota_get(context, project_id, resource, user_id=user_id)
def quota_get_all_by_project_and_user(context, project_id, user_id): def quota_get_all_by_project_and_user(context, project_id, user_id):
@ -152,6 +148,13 @@ def quota_get_all_by_project_and_user(context, project_id, user_id):
return IMPL.quota_get_all_by_project_and_user(context, project_id, user_id) return IMPL.quota_get_all_by_project_and_user(context, project_id, user_id)
def quota_get_all_by_project_and_share_type(context, project_id,
share_type_id):
"""Retrieve all quotas associated with a given project and user."""
return IMPL.quota_get_all_by_project_and_share_type(
context, project_id, share_type_id)
def quota_get_all_by_project(context, project_id): def quota_get_all_by_project(context, project_id):
"""Retrieve all quotas associated with a given project.""" """Retrieve all quotas associated with a given project."""
return IMPL.quota_get_all_by_project(context, project_id) return IMPL.quota_get_all_by_project(context, project_id)
@ -162,10 +165,11 @@ def quota_get_all(context, project_id):
return IMPL.quota_get_all(context, project_id) return IMPL.quota_get_all(context, project_id)
def quota_update(context, project_id, resource, limit, user_id=None): def quota_update(context, project_id, resource, limit, user_id=None,
share_type_id=None):
"""Update a quota or raise if it does not exist.""" """Update a quota or raise if it does not exist."""
return IMPL.quota_update(context, project_id, resource, limit, return IMPL.quota_update(context, project_id, resource, limit,
user_id=user_id) user_id=user_id, share_type_id=share_type_id)
################### ###################
@ -199,9 +203,12 @@ def quota_class_update(context, class_name, resource, limit):
################### ###################
def quota_usage_get(context, project_id, resource, user_id=None): def quota_usage_get(context, project_id, resource, user_id=None,
share_type_id=None):
"""Retrieve a quota usage or raise if it does not exist.""" """Retrieve a quota usage or raise if it does not exist."""
return IMPL.quota_usage_get(context, project_id, resource, user_id=user_id) return IMPL.quota_usage_get(
context, project_id, resource, user_id=user_id,
share_type_id=share_type_id)
def quota_usage_get_all_by_project_and_user(context, project_id, user_id): def quota_usage_get_all_by_project_and_user(context, project_id, user_id):
@ -210,47 +217,61 @@ def quota_usage_get_all_by_project_and_user(context, project_id, user_id):
project_id, user_id) project_id, user_id)
def quota_usage_get_all_by_project_and_share_type(context, project_id,
share_type_id):
"""Retrieve all usage associated with a given resource."""
return IMPL.quota_usage_get_all_by_project_and_share_type(
context, project_id, share_type_id)
def quota_usage_get_all_by_project(context, project_id): def quota_usage_get_all_by_project(context, project_id):
"""Retrieve all usage associated with a given resource.""" """Retrieve all usage associated with a given resource."""
return IMPL.quota_usage_get_all_by_project(context, project_id) return IMPL.quota_usage_get_all_by_project(context, project_id)
def quota_usage_create(context, project_id, user_id, resource, in_use, def quota_usage_create(context, project_id, user_id, resource, in_use,
reserved=0, until_refresh=None): reserved=0, until_refresh=None, share_type_id=None):
"""Create a quota usage.""" """Create a quota usage."""
return IMPL.quota_usage_create(context, project_id, user_id, resource, return IMPL.quota_usage_create(
in_use, reserved, until_refresh) context, project_id, user_id, resource, in_use, reserved,
until_refresh, share_type_id=share_type_id)
def quota_usage_update(context, project_id, user_id, resource, **kwargs): def quota_usage_update(context, project_id, user_id, resource,
share_type_id=None, **kwargs):
"""Update a quota usage or raise if it does not exist.""" """Update a quota usage or raise if it does not exist."""
return IMPL.quota_usage_update(context, project_id, user_id, resource, return IMPL.quota_usage_update(
**kwargs) context, project_id, user_id, resource, share_type_id=share_type_id,
**kwargs)
################### ###################
def quota_reserve(context, resources, quotas, user_quotas, deltas, expire, def quota_reserve(context, resources, quotas, user_quotas, share_type_quotas,
until_refresh, max_age, project_id=None, user_id=None): deltas, expire, until_refresh, max_age,
project_id=None, user_id=None, share_type_id=None):
"""Check quotas and create appropriate reservations.""" """Check quotas and create appropriate reservations."""
return IMPL.quota_reserve(context, resources, quotas, user_quotas, deltas, return IMPL.quota_reserve(
expire, until_refresh, max_age, context, resources, quotas, user_quotas, share_type_quotas, deltas,
project_id=project_id, user_id=user_id) expire, until_refresh, max_age, project_id=project_id, user_id=user_id,
share_type_id=share_type_id)
def reservation_commit(context, reservations, project_id=None, user_id=None): def reservation_commit(context, reservations, project_id=None, user_id=None,
share_type_id=None):
"""Commit quota reservations.""" """Commit quota reservations."""
return IMPL.reservation_commit(context, reservations, return IMPL.reservation_commit(
project_id=project_id, context, reservations, project_id=project_id, user_id=user_id,
user_id=user_id) share_type_id=share_type_id)
def reservation_rollback(context, reservations, project_id=None, user_id=None): def reservation_rollback(context, reservations, project_id=None, user_id=None,
share_type_id=None):
"""Roll back quota reservations.""" """Roll back quota reservations."""
return IMPL.reservation_rollback(context, reservations, return IMPL.reservation_rollback(
project_id=project_id, context, reservations, project_id=project_id, user_id=user_id,
user_id=user_id) share_type_id=share_type_id)
def quota_destroy_all_by_project_and_user(context, project_id, user_id): def quota_destroy_all_by_project_and_user(context, project_id, user_id):
@ -259,6 +280,13 @@ def quota_destroy_all_by_project_and_user(context, project_id, user_id):
project_id, user_id) project_id, user_id)
def quota_destroy_all_by_project_and_share_type(context, project_id,
share_type_id):
"""Destroy all quotas associated with a given project and user."""
return IMPL.quota_destroy_all_by_project_and_share_type(
context, project_id, share_type_id)
def quota_destroy_all_by_project(context, project_id): def quota_destroy_all_by_project(context, project_id):
"""Destroy all quotas associated with a given project.""" """Destroy all quotas associated with a given project."""
return IMPL.quota_destroy_all_by_project(context, project_id) return IMPL.quota_destroy_all_by_project(context, project_id)
@ -770,6 +798,13 @@ def share_network_remove_security_service(context, id, security_service_id):
security_service_id) security_service_id)
def count_share_networks(context, project_id, user_id=None,
share_type_id=None, session=None):
return IMPL.count_share_networks(
context, project_id, user_id=user_id, share_type_id=share_type_id,
session=session,
)
################## ##################

View File

@ -23,21 +23,15 @@ revision = '7d142971c4ef'
down_revision = 'd5db24264f5c' down_revision = 'd5db24264f5c'
from alembic import op from alembic import op
from sqlalchemy import Index, MetaData, Table
def _reservation_index(method): INDEX_NAME = 'reservations_deleted_expire_idx'
meta = MetaData() TABLE_NAME = 'reservations'
meta.bind = op.get_bind().engine
reservations = Table('reservations', meta, autoload=True)
index = Index('reservations_deleted_expire_idx',
reservations.c.deleted, reservations.c.expire)
getattr(index, method)(meta.bind)
def upgrade(): def upgrade():
_reservation_index('create') op.create_index(INDEX_NAME, TABLE_NAME, ['deleted', 'expire'])
def downgrade(): def downgrade():
_reservation_index('drop') op.drop_index(INDEX_NAME, TABLE_NAME)

View File

@ -0,0 +1,63 @@
# 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 ProjectShareTypeQuota model
Revision ID: b516de97bfee
Revises: 238720805ce1
Create Date: 2017-03-27 15:11:11.449617
"""
# revision identifiers, used by Alembic.
revision = 'b516de97bfee'
down_revision = '238720805ce1'
from alembic import op
import sqlalchemy as sql
NEW_TABLE_NAME = 'project_share_type_quotas'
def upgrade():
op.create_table(
NEW_TABLE_NAME,
sql.Column('id', sql.Integer, primary_key=True, nullable=False),
sql.Column('project_id', sql.String(length=255)),
sql.Column('resource', sql.String(length=255), nullable=False),
sql.Column('hard_limit', sql.Integer, nullable=True),
sql.Column('created_at', sql.DateTime),
sql.Column('updated_at', sql.DateTime),
sql.Column('deleted_at', sql.DateTime),
sql.Column('deleted', sql.Integer, default=0),
sql.Column(
'share_type_id', sql.String(36),
sql.ForeignKey(
'share_types.id', name='share_type_id_fk',
),
nullable=False),
sql.UniqueConstraint(
'share_type_id', 'resource', 'deleted',
name="uc_quotas_per_share_types"),
mysql_engine='InnoDB',
)
for table_name in ('quota_usages', 'reservations'):
op.add_column(
table_name,
sql.Column('share_type_id', sql.String(36), nullable=True),
)
def downgrade():
op.drop_table(NEW_TABLE_NAME)
for table_name in ('quota_usages', 'reservations'):
op.drop_column(table_name, 'share_type_id')

View File

@ -35,6 +35,7 @@ from oslo_db import options as db_options
from oslo_db.sqlalchemy import session from oslo_db.sqlalchemy import session
from oslo_db.sqlalchemy import utils as db_utils from oslo_db.sqlalchemy import utils as db_utils
from oslo_log import log from oslo_log import log
from oslo_utils import excutils
from oslo_utils import timeutils from oslo_utils import timeutils
from oslo_utils import uuidutils from oslo_utils import uuidutils
import six import six
@ -279,40 +280,41 @@ def ensure_model_dict_has_id(model_dict):
return model_dict return model_dict
def _sync_shares(context, project_id, user_id, session): def _sync_shares(context, project_id, user_id, session, share_type_id=None):
(shares, gigs) = share_data_get_for_project(context, (shares, gigs) = share_data_get_for_project(
project_id, context, project_id, user_id, share_type_id=share_type_id,
user_id, session=session)
session=session)
return {'shares': shares} return {'shares': shares}
def _sync_snapshots(context, project_id, user_id, session): def _sync_snapshots(context, project_id, user_id, session, share_type_id=None):
(snapshots, gigs) = snapshot_data_get_for_project(context, (snapshots, gigs) = snapshot_data_get_for_project(
project_id, context, project_id, user_id, share_type_id=share_type_id,
user_id, session=session)
session=session)
return {'snapshots': snapshots} return {'snapshots': snapshots}
def _sync_gigabytes(context, project_id, user_id, session): def _sync_gigabytes(context, project_id, user_id, session, share_type_id=None):
_junk, share_gigs = share_data_get_for_project( _junk, share_gigs = share_data_get_for_project(
context, project_id, user_id, session=session) context, project_id, user_id, share_type_id=share_type_id,
return dict(gigabytes=share_gigs) session=session)
return {"gigabytes": share_gigs}
def _sync_snapshot_gigabytes(context, project_id, user_id, session): def _sync_snapshot_gigabytes(context, project_id, user_id, session,
share_type_id=None):
_junk, snapshot_gigs = snapshot_data_get_for_project( _junk, snapshot_gigs = snapshot_data_get_for_project(
context, project_id, user_id, session=session) context, project_id, user_id, share_type_id=share_type_id,
return dict(snapshot_gigabytes=snapshot_gigs) session=session)
return {"snapshot_gigabytes": snapshot_gigs}
def _sync_share_networks(context, project_id, user_id, session): def _sync_share_networks(context, project_id, user_id, session,
share_networks = share_network_get_all_by_project(context, share_type_id=None):
project_id, share_networks_count = count_share_networks(
user_id, context, project_id, user_id, share_type_id=share_type_id,
session=session) session=session)
return {'share_networks': len(share_networks)} return {'share_networks': share_networks_count}
QUOTA_SYNC_FUNCTIONS = { QUOTA_SYNC_FUNCTIONS = {
@ -459,50 +461,60 @@ def service_update(context, service_id, values):
################### ###################
@require_context
def quota_get(context, project_id, resource, session=None):
result = (model_query(context, models.Quota, session=session,
read_deleted="no").
filter_by(project_id=project_id).
filter_by(resource=resource).
first())
if not result:
raise exception.ProjectQuotaNotFound(project_id=project_id)
return result
@require_context @require_context
def quota_get_all_by_project_and_user(context, project_id, user_id): def quota_get_all_by_project_and_user(context, project_id, user_id):
authorize_project_context(context, project_id) authorize_project_context(context, project_id)
user_quotas = model_query(
user_quotas = (model_query(context, models.ProjectUserQuota, context, models.ProjectUserQuota,
models.ProjectUserQuota.resource, models.ProjectUserQuota.resource,
models.ProjectUserQuota.hard_limit). models.ProjectUserQuota.hard_limit,
filter_by(project_id=project_id). ).filter_by(
filter_by(user_id=user_id). project_id=project_id,
all()) ).filter_by(
user_id=user_id,
).all()
result = {'project_id': project_id, 'user_id': user_id} result = {'project_id': project_id, 'user_id': user_id}
for quota in user_quotas: for quota in user_quotas:
result[quota.resource] = quota.hard_limit result[quota.resource] = quota.hard_limit
return result
@require_context
def quota_get_all_by_project_and_share_type(context, project_id,
share_type_id):
authorize_project_context(context, project_id)
share_type_quotas = model_query(
context, models.ProjectShareTypeQuota,
models.ProjectShareTypeQuota.resource,
models.ProjectShareTypeQuota.hard_limit,
).filter_by(
project_id=project_id,
).filter_by(
share_type_id=share_type_id,
).all()
result = {
'project_id': project_id,
'share_type_id': share_type_id,
}
for quota in share_type_quotas:
result[quota.resource] = quota.hard_limit
return result return result
@require_context @require_context
def quota_get_all_by_project(context, project_id): def quota_get_all_by_project(context, project_id):
authorize_project_context(context, project_id) authorize_project_context(context, project_id)
project_quotas = model_query(
rows = (model_query(context, models.Quota, read_deleted="no"). context, models.Quota, read_deleted="no",
filter_by(project_id=project_id). ).filter_by(
all()) project_id=project_id,
).all()
result = {'project_id': project_id} result = {'project_id': project_id}
for row in rows: for quota in project_quotas:
result[row.resource] = row.hard_limit result[quota.resource] = quota.hard_limit
return result return result
@ -518,26 +530,35 @@ def quota_get_all(context, project_id):
@require_admin_context @require_admin_context
def quota_create(context, project_id, resource, limit, user_id=None): def quota_create(context, project_id, resource, limit, user_id=None,
share_type_id=None):
per_user = user_id and resource not in PER_PROJECT_QUOTAS per_user = user_id and resource not in PER_PROJECT_QUOTAS
if per_user: if per_user:
check = (model_query(context, models.ProjectUserQuota). check = model_query(context, models.ProjectUserQuota).filter(
filter_by(project_id=project_id). models.ProjectUserQuota.project_id == project_id,
filter_by(user_id=user_id). models.ProjectUserQuota.user_id == user_id,
filter_by(resource=resource). models.ProjectUserQuota.resource == resource,
all()) ).all()
quota_ref = models.ProjectUserQuota()
quota_ref.user_id = user_id
elif share_type_id:
check = model_query(context, models.ProjectShareTypeQuota).filter(
models.ProjectShareTypeQuota.project_id == project_id,
models.ProjectShareTypeQuota.share_type_id == share_type_id,
models.ProjectShareTypeQuota.resource == resource,
).all()
quota_ref = models.ProjectShareTypeQuota()
quota_ref.share_type_id = share_type_id
else: else:
check = (model_query(context, models.Quota). check = model_query(context, models.Quota).filter(
filter_by(project_id=project_id). models.Quota.project_id == project_id,
filter_by(resource=resource). models.Quota.resource == resource,
all()) ).all()
quota_ref = models.Quota()
if check: if check:
raise exception.QuotaExists(project_id=project_id, resource=resource) raise exception.QuotaExists(project_id=project_id, resource=resource)
quota_ref = models.ProjectUserQuota() if per_user else models.Quota()
if per_user:
quota_ref.user_id = user_id
quota_ref.project_id = project_id quota_ref.project_id = project_id
quota_ref.resource = resource quota_ref.resource = resource
quota_ref.hard_limit = limit quota_ref.hard_limit = limit
@ -549,22 +570,36 @@ def quota_create(context, project_id, resource, limit, user_id=None):
@require_admin_context @require_admin_context
@oslo_db_api.wrap_db_retry(max_retries=5, retry_on_deadlock=True) @oslo_db_api.wrap_db_retry(max_retries=5, retry_on_deadlock=True)
def quota_update(context, project_id, resource, limit, user_id=None): def quota_update(context, project_id, resource, limit, user_id=None,
share_type_id=None):
per_user = user_id and resource not in PER_PROJECT_QUOTAS per_user = user_id and resource not in PER_PROJECT_QUOTAS
model = models.ProjectUserQuota if per_user else models.Quota
query = (model_query(context, model).
filter_by(project_id=project_id).
filter_by(resource=resource))
if per_user: if per_user:
query = query.filter_by(user_id=user_id) query = model_query(context, models.ProjectUserQuota).filter(
models.ProjectUserQuota.project_id == project_id,
models.ProjectUserQuota.user_id == user_id,
models.ProjectUserQuota.resource == resource,
)
elif share_type_id:
query = model_query(context, models.ProjectShareTypeQuota).filter(
models.ProjectShareTypeQuota.project_id == project_id,
models.ProjectShareTypeQuota.share_type_id == share_type_id,
models.ProjectShareTypeQuota.resource == resource,
)
else:
query = model_query(context, models.Quota).filter(
models.Quota.project_id == project_id,
models.Quota.resource == resource,
)
result = query.update({'hard_limit': limit}) result = query.update({'hard_limit': limit})
if not result: if not result:
if per_user: if per_user:
raise exception.ProjectUserQuotaNotFound(project_id=project_id, raise exception.ProjectUserQuotaNotFound(
user_id=user_id) project_id=project_id, user_id=user_id)
else: elif share_type_id:
raise exception.ProjectQuotaNotFound(project_id=project_id) raise exception.ProjectShareTypeQuotaNotFound(
project_id=project_id, share_type=share_type_id)
raise exception.ProjectQuotaNotFound(project_id=project_id)
################### ###################
@ -640,7 +675,8 @@ def quota_class_update(context, class_name, resource, limit):
@require_context @require_context
def quota_usage_get(context, project_id, resource, user_id=None): def quota_usage_get(context, project_id, resource, user_id=None,
share_type_id=None):
query = (model_query(context, models.QuotaUsage, read_deleted="no"). query = (model_query(context, models.QuotaUsage, read_deleted="no").
filter_by(project_id=project_id). filter_by(project_id=project_id).
filter_by(resource=resource)) filter_by(resource=resource))
@ -649,6 +685,8 @@ def quota_usage_get(context, project_id, resource, user_id=None):
result = query.filter_by(user_id=user_id).first() result = query.filter_by(user_id=user_id).first()
else: else:
result = query.filter_by(user_id=None).first() result = query.filter_by(user_id=None).first()
elif share_type_id:
result = query.filter_by(queryshare_type_id=share_type_id).first()
else: else:
result = query.first() result = query.first()
@ -658,7 +696,8 @@ def quota_usage_get(context, project_id, resource, user_id=None):
return result return result
def _quota_usage_get_all(context, project_id, user_id=None): def _quota_usage_get_all(context, project_id, user_id=None,
share_type_id=None):
authorize_project_context(context, project_id) authorize_project_context(context, project_id)
query = (model_query(context, models.QuotaUsage, read_deleted="no"). query = (model_query(context, models.QuotaUsage, read_deleted="no").
filter_by(project_id=project_id)) filter_by(project_id=project_id))
@ -667,6 +706,11 @@ def _quota_usage_get_all(context, project_id, user_id=None):
query = query.filter(or_(models.QuotaUsage.user_id == user_id, query = query.filter(or_(models.QuotaUsage.user_id == user_id,
models.QuotaUsage.user_id is None)) models.QuotaUsage.user_id is None))
result['user_id'] = user_id result['user_id'] = user_id
elif share_type_id:
query = query.filter_by(share_type_id=share_type_id)
result['share_type_id'] = share_type_id
else:
query = query.filter_by(share_type_id=None)
rows = query.all() rows = query.all()
for row in rows: for row in rows:
@ -690,11 +734,22 @@ def quota_usage_get_all_by_project_and_user(context, project_id, user_id):
return _quota_usage_get_all(context, project_id, user_id=user_id) return _quota_usage_get_all(context, project_id, user_id=user_id)
@require_context
def quota_usage_get_all_by_project_and_share_type(context, project_id,
share_type_id):
return _quota_usage_get_all(
context, project_id, share_type_id=share_type_id)
def _quota_usage_create(context, project_id, user_id, resource, in_use, def _quota_usage_create(context, project_id, user_id, resource, in_use,
reserved, until_refresh, session=None): reserved, until_refresh, share_type_id=None,
session=None):
quota_usage_ref = models.QuotaUsage() quota_usage_ref = models.QuotaUsage()
if share_type_id:
quota_usage_ref.share_type_id = share_type_id
else:
quota_usage_ref.user_id = user_id
quota_usage_ref.project_id = project_id quota_usage_ref.project_id = project_id
quota_usage_ref.user_id = user_id
quota_usage_ref.resource = resource quota_usage_ref.resource = resource
quota_usage_ref.in_use = in_use quota_usage_ref.in_use = in_use
quota_usage_ref.reserved = reserved quota_usage_ref.reserved = reserved
@ -709,27 +764,31 @@ def _quota_usage_create(context, project_id, user_id, resource, in_use,
@require_admin_context @require_admin_context
def quota_usage_create(context, project_id, user_id, resource, in_use, def quota_usage_create(context, project_id, user_id, resource, in_use,
reserved, until_refresh): reserved, until_refresh, share_type_id=None):
session = get_session() session = get_session()
return _quota_usage_create(context, project_id, user_id, resource, in_use, return _quota_usage_create(
reserved, until_refresh, session) context, project_id, user_id, resource, in_use, reserved,
until_refresh, share_type_id=share_type_id, session=session)
@require_admin_context @require_admin_context
@oslo_db_api.wrap_db_retry(max_retries=5, retry_on_deadlock=True) @oslo_db_api.wrap_db_retry(max_retries=5, retry_on_deadlock=True)
def quota_usage_update(context, project_id, user_id, resource, **kwargs): def quota_usage_update(context, project_id, user_id, resource,
share_type_id=None, **kwargs):
updates = {} updates = {}
for key in ('in_use', 'reserved', 'until_refresh'):
for key in ['in_use', 'reserved', 'until_refresh']:
if key in kwargs: if key in kwargs:
updates[key] = kwargs[key] updates[key] = kwargs[key]
result = (model_query(context, models.QuotaUsage, read_deleted="no"). query = model_query(
filter_by(project_id=project_id). context, models.QuotaUsage, read_deleted="no",
filter_by(resource=resource). ).filter_by(project_id=project_id).filter_by(resource=resource)
filter(or_(models.QuotaUsage.user_id == user_id, if share_type_id:
models.QuotaUsage.user_id is None)). query = query.filter_by(share_type_id=share_type_id)
update(updates)) else:
query = query.filter(or_(models.QuotaUsage.user_id == user_id,
models.QuotaUsage.user_id is None))
result = query.update(updates)
if not result: if not result:
raise exception.QuotaUsageNotFound(project_id=project_id) raise exception.QuotaUsageNotFound(project_id=project_id)
@ -739,12 +798,15 @@ def quota_usage_update(context, project_id, user_id, resource, **kwargs):
def _reservation_create(context, uuid, usage, project_id, user_id, resource, def _reservation_create(context, uuid, usage, project_id, user_id, resource,
delta, expire, session=None): delta, expire, share_type_id=None, session=None):
reservation_ref = models.Reservation() reservation_ref = models.Reservation()
reservation_ref.uuid = uuid reservation_ref.uuid = uuid
reservation_ref.usage_id = usage['id'] reservation_ref.usage_id = usage['id']
reservation_ref.project_id = project_id reservation_ref.project_id = project_id
reservation_ref.user_id = user_id if share_type_id:
reservation_ref.share_type_id = share_type_id
else:
reservation_ref.user_id = user_id
reservation_ref.resource = resource reservation_ref.resource = resource
reservation_ref.delta = delta reservation_ref.delta = delta
reservation_ref.expire = expire reservation_ref.expire = expire
@ -760,6 +822,16 @@ def _reservation_create(context, uuid, usage, project_id, user_id, resource,
# code always acquires the lock on quota_usages before acquiring the lock # code always acquires the lock on quota_usages before acquiring the lock
# on reservations. # on reservations.
def _get_share_type_quota_usages(context, session, project_id, share_type_id):
rows = model_query(
context, models.QuotaUsage, read_deleted="no", session=session,
).filter(
models.QuotaUsage.project_id == project_id,
models.QuotaUsage.share_type_id == share_type_id,
).with_lockmode('update').all()
return {row.resource: row for row in rows}
def _get_user_quota_usages(context, session, project_id, user_id): def _get_user_quota_usages(context, session, project_id, user_id):
# Broken out for testability # Broken out for testability
rows = (model_query(context, models.QuotaUsage, rows = (model_query(context, models.QuotaUsage,
@ -778,6 +850,7 @@ def _get_project_quota_usages(context, session, project_id):
read_deleted="no", read_deleted="no",
session=session). session=session).
filter_by(project_id=project_id). filter_by(project_id=project_id).
filter(models.QuotaUsage.share_type_id is None).
with_lockmode('update'). with_lockmode('update').
all()) all())
result = dict() result = dict()
@ -795,24 +868,49 @@ def _get_project_quota_usages(context, session, project_id):
@require_context @require_context
def quota_reserve(context, resources, project_quotas, user_quotas,
share_type_quotas, deltas, expire, until_refresh,
max_age, project_id=None, user_id=None, share_type_id=None):
user_reservations = _quota_reserve(
context, resources, project_quotas, user_quotas,
deltas, expire, until_refresh, max_age, project_id, user_id=user_id)
if share_type_id:
try:
st_reservations = _quota_reserve(
context, resources, project_quotas, share_type_quotas,
deltas, expire, until_refresh, max_age, project_id,
share_type_id=share_type_id)
except exception.OverQuota:
with excutils.save_and_reraise_exception():
# rollback previous reservations
reservation_rollback(
context, user_reservations,
project_id=project_id, user_id=user_id)
return user_reservations + st_reservations
return user_reservations
@oslo_db_api.wrap_db_retry(max_retries=5, retry_on_deadlock=True) @oslo_db_api.wrap_db_retry(max_retries=5, retry_on_deadlock=True)
def quota_reserve(context, resources, project_quotas, user_quotas, deltas, def _quota_reserve(context, resources, project_quotas, user_or_st_quotas,
expire, until_refresh, max_age, project_id=None, deltas, expire, until_refresh,
user_id=None): max_age, project_id=None, user_id=None, share_type_id=None):
elevated = context.elevated() elevated = context.elevated()
session = get_session() session = get_session()
with session.begin(): with session.begin():
if project_id is None: if project_id is None:
project_id = context.project_id project_id = context.project_id
if user_id is None: if share_type_id:
user_id = context.user_id user_or_st_usages = _get_share_type_quota_usages(
context, session, project_id, share_type_id)
else:
user_id = user_id if user_id else context.user_id
user_or_st_usages = _get_user_quota_usages(
context, session, project_id, user_id)
# Get the current usages # Get the current usages
user_usages = _get_user_quota_usages(context, session, project_usages = _get_project_quota_usages(
project_id, user_id) context, session, project_id)
project_usages = _get_project_quota_usages(context, session,
project_id)
# Handle usage refresh # Handle usage refresh
work = set(deltas.keys()) work = set(deltas.keys())
@ -822,36 +920,38 @@ def quota_reserve(context, resources, project_quotas, user_quotas, deltas,
# Do we need to refresh the usage? # Do we need to refresh the usage?
refresh = False refresh = False
if ((resource not in PER_PROJECT_QUOTAS) and if ((resource not in PER_PROJECT_QUOTAS) and
(resource not in user_usages)): (resource not in user_or_st_usages)):
user_usages[resource] = _quota_usage_create( user_or_st_usages[resource] = _quota_usage_create(
elevated, elevated,
project_id, project_id,
user_id, user_id,
resource, resource,
0, 0, 0, 0,
until_refresh or None, until_refresh or None,
share_type_id=share_type_id,
session=session) session=session)
refresh = True refresh = True
elif ((resource in PER_PROJECT_QUOTAS) and elif ((resource in PER_PROJECT_QUOTAS) and
(resource not in user_usages)): (resource not in user_or_st_usages)):
user_usages[resource] = _quota_usage_create( user_or_st_usages[resource] = _quota_usage_create(
elevated, elevated,
project_id, project_id,
None, None,
resource, resource,
0, 0, 0, 0,
until_refresh or None, until_refresh or None,
share_type_id=share_type_id,
session=session) session=session)
refresh = True refresh = True
elif user_usages[resource].in_use < 0: elif user_or_st_usages[resource].in_use < 0:
# Negative in_use count indicates a desync, so try to # Negative in_use count indicates a desync, so try to
# heal from that... # heal from that...
refresh = True refresh = True
elif user_usages[resource].until_refresh is not None: elif user_or_st_usages[resource].until_refresh is not None:
user_usages[resource].until_refresh -= 1 user_or_st_usages[resource].until_refresh -= 1
if user_usages[resource].until_refresh <= 0: if user_or_st_usages[resource].until_refresh <= 0:
refresh = True refresh = True
elif max_age and (user_usages[resource].updated_at - elif max_age and (user_or_st_usages[resource].updated_at -
timeutils.utcnow()).seconds >= max_age: timeutils.utcnow()).seconds >= max_age:
refresh = True refresh = True
@ -860,46 +960,54 @@ def quota_reserve(context, resources, project_quotas, user_quotas, deltas,
# Grab the sync routine # Grab the sync routine
sync = QUOTA_SYNC_FUNCTIONS[resources[resource].sync] sync = QUOTA_SYNC_FUNCTIONS[resources[resource].sync]
updates = sync(elevated, project_id, user_id, session) updates = sync(
elevated, project_id, user_id,
share_type_id=share_type_id, session=session)
for res, in_use in updates.items(): for res, in_use in updates.items():
# Make sure we have a destination for the usage! # Make sure we have a destination for the usage!
if ((res not in PER_PROJECT_QUOTAS) and if ((res not in PER_PROJECT_QUOTAS) and
(res not in user_usages)): (res not in user_or_st_usages)):
user_usages[res] = _quota_usage_create( user_or_st_usages[res] = _quota_usage_create(
elevated, elevated,
project_id, project_id,
user_id, user_id,
res, res,
0, 0, 0, 0,
until_refresh or None, until_refresh or None,
share_type_id=share_type_id,
session=session) session=session)
if ((res in PER_PROJECT_QUOTAS) and if ((res in PER_PROJECT_QUOTAS) and
(res not in user_usages)): (res not in user_or_st_usages)):
user_usages[res] = _quota_usage_create( user_or_st_usages[res] = _quota_usage_create(
elevated, elevated,
project_id, project_id,
None, None,
res, res,
0, 0, 0, 0,
until_refresh or None, until_refresh or None,
share_type_id=share_type_id,
session=session) session=session)
if user_usages[res].in_use != in_use: if user_or_st_usages[res].in_use != in_use:
LOG.debug('quota_usages out of sync, updating. ' LOG.debug(
'project_id: %(project_id)s, ' 'quota_usages out of sync, updating. '
'user_id: %(user_id)s, ' 'project_id: %(project_id)s, '
'resource: %(res)s, ' 'user_id: %(user_id)s, '
'tracked usage: %(tracked_use)s, ' 'share_type_id: %(share_type_id)s, '
'actual usage: %(in_use)s', 'resource: %(res)s, '
{'project_id': project_id, 'tracked usage: %(tracked_use)s, '
'user_id': user_id, 'actual usage: %(in_use)s',
'res': res, {'project_id': project_id,
'tracked_use': user_usages[res].in_use, 'user_id': user_id,
'in_use': in_use}) 'share_type_id': share_type_id,
'res': res,
'tracked_use': user_or_st_usages[res].in_use,
'in_use': in_use})
# Update the usage # Update the usage
user_usages[res].in_use = in_use user_or_st_usages[res].in_use = in_use
user_usages[res].until_refresh = until_refresh or None user_or_st_usages[res].until_refresh = (
until_refresh or None)
# Because more than one resource may be refreshed # Because more than one resource may be refreshed
# by the call to the sync routine, and we don't # by the call to the sync routine, and we don't
@ -916,22 +1024,22 @@ def quota_reserve(context, resources, project_quotas, user_quotas, deltas,
# Check for deltas that would go negative # Check for deltas that would go negative
unders = [res for res, delta in deltas.items() unders = [res for res, delta in deltas.items()
if delta < 0 and if delta < 0 and
delta + user_usages[res].in_use < 0] delta + user_or_st_usages[res].in_use < 0]
# Now, let's check the quotas # Now, let's check the quotas
# NOTE(Vek): We're only concerned about positive increments. # NOTE(Vek): We're only concerned about positive increments.
# If a project has gone over quota, we want them to # If a project has gone over quota, we want them to
# be able to reduce their usage without any # be able to reduce their usage without any
# problems. # problems.
for key, value in user_usages.items(): for key, value in user_or_st_usages.items():
if key not in project_usages: if key not in project_usages:
project_usages[key] = value project_usages[key] = value
overs = [res for res, delta in deltas.items() overs = [res for res, delta in deltas.items()
if user_quotas[res] >= 0 and delta >= 0 and if user_or_st_quotas[res] >= 0 and delta >= 0 and
(project_quotas[res] < delta + (project_quotas[res] < delta +
project_usages[res]['total'] or project_usages[res]['total'] or
user_quotas[res] < delta + user_or_st_quotas[res] < delta +
user_usages[res].total)] user_or_st_usages[res].total)]
# NOTE(Vek): The quota check needs to be in the transaction, # NOTE(Vek): The quota check needs to be in the transaction,
# but the transaction doesn't fail just because # but the transaction doesn't fail just because
@ -946,10 +1054,11 @@ def quota_reserve(context, resources, project_quotas, user_quotas, deltas,
for res, delta in deltas.items(): for res, delta in deltas.items():
reservation = _reservation_create(elevated, reservation = _reservation_create(elevated,
uuidutils.generate_uuid(), uuidutils.generate_uuid(),
user_usages[res], user_or_st_usages[res],
project_id, project_id,
user_id, user_id,
res, delta, expire, res, delta, expire,
share_type_id=share_type_id,
session=session) session=session)
reservations.append(reservation.uuid) reservations.append(reservation.uuid)
@ -966,24 +1075,24 @@ def quota_reserve(context, resources, project_quotas, user_quotas, deltas,
# To prevent this, we only update the # To prevent this, we only update the
# reserved value if the delta is positive. # reserved value if the delta is positive.
if delta > 0: if delta > 0:
user_usages[res].reserved += delta user_or_st_usages[res].reserved += delta
# Apply updates to the usages table # Apply updates to the usages table
for usage_ref in user_usages.values(): for usage_ref in user_or_st_usages.values():
session.add(usage_ref) session.add(usage_ref)
if unders: if unders:
LOG.warning("Change will make usage less than 0 for the following " LOG.warning("Change will make usage less than 0 for the following "
"resources: %s", unders) "resources: %s", unders)
if overs: if overs:
if project_quotas == user_quotas: if project_quotas == user_or_st_quotas:
usages = project_usages usages = project_usages
else: else:
usages = user_usages usages = user_or_st_usages
usages = {k: dict(in_use=v['in_use'], reserved=v['reserved']) usages = {k: dict(in_use=v['in_use'], reserved=v['reserved'])
for k, v in usages.items()} for k, v in usages.items()}
raise exception.OverQuota(overs=sorted(overs), quotas=user_quotas, raise exception.OverQuota(
usages=usages) overs=sorted(overs), quotas=user_or_st_quotas, usages=usages)
return reservations return reservations
@ -1001,13 +1110,25 @@ def _quota_reservations_query(session, context, reservations):
@require_context @require_context
@oslo_db_api.wrap_db_retry(max_retries=5, retry_on_deadlock=True) @oslo_db_api.wrap_db_retry(max_retries=5, retry_on_deadlock=True)
def reservation_commit(context, reservations, project_id=None, user_id=None): def reservation_commit(context, reservations, project_id=None, user_id=None,
share_type_id=None):
session = get_session() session = get_session()
with session.begin(): with session.begin():
usages = _get_user_quota_usages(context, session, project_id, user_id) if share_type_id:
reservation_query = _quota_reservations_query(session, context, st_usages = _get_share_type_quota_usages(
reservations) context, session, project_id, share_type_id)
else:
st_usages = {}
user_usages = _get_user_quota_usages(
context, session, project_id, user_id)
reservation_query = _quota_reservations_query(
session, context, reservations)
for reservation in reservation_query.all(): for reservation in reservation_query.all():
if reservation['share_type_id']:
usages = st_usages
else:
usages = user_usages
usage = usages[reservation.resource] usage = usages[reservation.resource]
if reservation.delta >= 0: if reservation.delta >= 0:
usage.reserved -= reservation.delta usage.reserved -= reservation.delta
@ -1017,13 +1138,25 @@ def reservation_commit(context, reservations, project_id=None, user_id=None):
@require_context @require_context
@oslo_db_api.wrap_db_retry(max_retries=5, retry_on_deadlock=True) @oslo_db_api.wrap_db_retry(max_retries=5, retry_on_deadlock=True)
def reservation_rollback(context, reservations, project_id=None, user_id=None): def reservation_rollback(context, reservations, project_id=None, user_id=None,
share_type_id=None):
session = get_session() session = get_session()
with session.begin(): with session.begin():
usages = _get_user_quota_usages(context, session, project_id, user_id) if share_type_id:
reservation_query = _quota_reservations_query(session, context, st_usages = _get_share_type_quota_usages(
reservations) context, session, project_id, share_type_id)
else:
st_usages = {}
user_usages = _get_user_quota_usages(
context, session, project_id, user_id)
reservation_query = _quota_reservations_query(
session, context, reservations)
for reservation in reservation_query.all(): for reservation in reservation_query.all():
if reservation['share_type_id']:
usages = st_usages
else:
usages = user_usages
usage = usages[reservation.resource] usage = usages[reservation.resource]
if reservation.delta >= 0: if reservation.delta >= 0:
usage.reserved -= reservation.delta usage.reserved -= reservation.delta
@ -1050,6 +1183,37 @@ def quota_destroy_all_by_project_and_user(context, project_id, user_id):
filter_by(user_id=user_id).soft_delete(synchronize_session=False)) filter_by(user_id=user_id).soft_delete(synchronize_session=False))
@require_admin_context
def quota_destroy_all_by_project_and_share_type(context, project_id,
share_type_id):
session = get_session()
with session.begin():
model_query(
context, models.ProjectShareTypeQuota, session=session,
read_deleted="no",
).filter_by(
project_id=project_id,
).filter_by(
share_type_id=share_type_id,
).soft_delete(synchronize_session=False)
model_query(
context, models.QuotaUsage, session=session, read_deleted="no",
).filter_by(
project_id=project_id,
).filter_by(
share_type_id=share_type_id,
).soft_delete(synchronize_session=False)
model_query(
context, models.Reservation, session=session, read_deleted="no",
).filter_by(
project_id=project_id,
).filter_by(
share_type_id=share_type_id,
).soft_delete(synchronize_session=False)
@require_admin_context @require_admin_context
def quota_destroy_all_by_project(context, project_id): def quota_destroy_all_by_project(context, project_id):
session = get_session() session = get_session()
@ -1524,18 +1688,19 @@ def share_create(context, share_values, create_share_instance=True):
@require_admin_context @require_admin_context
def share_data_get_for_project(context, project_id, user_id, session=None): def share_data_get_for_project(context, project_id, user_id,
share_type_id=None, session=None):
query = (model_query(context, models.Share, query = (model_query(context, models.Share,
func.count(models.Share.id), func.count(models.Share.id),
func.sum(models.Share.size), func.sum(models.Share.size),
read_deleted="no", read_deleted="no",
session=session). session=session).
filter_by(project_id=project_id)) filter_by(project_id=project_id))
if user_id: if share_type_id:
result = query.filter_by(user_id=user_id).first() query = query.join("instances").filter_by(share_type_id=share_type_id)
else: elif user_id:
result = query.first() query = query.filter_by(user_id=user_id)
result = query.first()
return (result[0] or 0, result[1] or 0) return (result[0] or 0, result[1] or 0)
@ -2161,7 +2326,8 @@ def share_snapshot_create(context, create_values,
@require_admin_context @require_admin_context
def snapshot_data_get_for_project(context, project_id, user_id, session=None): def snapshot_data_get_for_project(context, project_id, user_id,
share_type_id=None, session=None):
query = (model_query(context, models.ShareSnapshot, query = (model_query(context, models.ShareSnapshot,
func.count(models.ShareSnapshot.id), func.count(models.ShareSnapshot.id),
func.sum(models.ShareSnapshot.size), func.sum(models.ShareSnapshot.size),
@ -2169,10 +2335,14 @@ def snapshot_data_get_for_project(context, project_id, user_id, session=None):
session=session). session=session).
filter_by(project_id=project_id)) filter_by(project_id=project_id))
if user_id: if share_type_id:
result = query.filter_by(user_id=user_id).first() query = query.join(
else: models.ShareInstance,
result = query.first() models.ShareInstance.share_id == models.ShareSnapshot.share_id,
).filter_by(share_type_id=share_type_id)
elif user_id:
query = query.filter_by(user_id=user_id)
result = query.first()
return (result[0] or 0, result[1] or 0) return (result[0] or 0, result[1] or 0)
@ -3046,13 +3216,8 @@ def share_network_get_all(context):
@require_context @require_context
def share_network_get_all_by_project(context, project_id, user_id=None, def share_network_get_all_by_project(context, project_id):
session=None): return _network_get_query(context).filter_by(project_id=project_id).all()
query = _network_get_query(context, session)
query = query.filter_by(project_id=project_id)
if user_id is not None:
query = query.filter_by(user_id=user_id)
return query.all()
@require_context @require_context
@ -3123,6 +3288,22 @@ def share_network_remove_security_service(context, id, security_service_id):
return share_nw_ref return share_nw_ref
@require_context
def count_share_networks(context, project_id, user_id=None,
share_type_id=None, session=None):
query = model_query(
context, models.ShareNetwork,
func.count(models.ShareNetwork.id),
read_deleted="no",
session=session).filter_by(project_id=project_id)
if share_type_id:
query = query.join("share_instances").filter_by(
share_type_id=share_type_id)
elif user_id is not None:
query = query.filter_by(user_id=user_id)
return query.first()[0]
################### ###################

View File

@ -108,6 +108,7 @@ class Quota(BASE, ManilaBase):
project_id = Column(String(255), index=True) project_id = Column(String(255), index=True)
resource = Column(String(255)) resource = Column(String(255))
hard_limit = Column(Integer, nullable=True) hard_limit = Column(Integer, nullable=True)
@ -121,6 +122,19 @@ class ProjectUserQuota(BASE, ManilaBase):
user_id = Column(String(255), nullable=False) user_id = Column(String(255), nullable=False)
resource = Column(String(255), nullable=False) resource = Column(String(255), nullable=False)
hard_limit = Column(Integer)
class ProjectShareTypeQuota(BASE, ManilaBase):
"""Represents a single quota override for a share type within a project."""
__tablename__ = 'project_share_type_quotas'
id = Column(Integer, primary_key=True, nullable=False)
project_id = Column(String(255), nullable=False)
share_type_id = Column(
String(36), ForeignKey('share_types.id'), nullable=False)
resource = Column(String(255), nullable=False)
hard_limit = Column(Integer) hard_limit = Column(Integer)
@ -149,6 +163,7 @@ class QuotaUsage(BASE, ManilaBase):
project_id = Column(String(255), index=True) project_id = Column(String(255), index=True)
user_id = Column(String(255)) user_id = Column(String(255))
share_type_id = Column(String(36))
resource = Column(String(255)) resource = Column(String(255))
in_use = Column(Integer) in_use = Column(Integer)
@ -172,6 +187,7 @@ class Reservation(BASE, ManilaBase):
project_id = Column(String(255), index=True) project_id = Column(String(255), index=True)
user_id = Column(String(255)) user_id = Column(String(255))
share_type_id = Column(String(36))
resource = Column(String(255)) resource = Column(String(255))
delta = Column(Integer) delta = Column(Integer)

View File

@ -330,6 +330,11 @@ class ProjectUserQuotaNotFound(QuotaNotFound):
"could not be found.") "could not be found.")
class ProjectShareTypeQuotaNotFound(QuotaNotFound):
message = _("Quota for share_type %(share_type)s in "
"project %(project_id)s could not be found.")
class ProjectQuotaNotFound(QuotaNotFound): class ProjectQuotaNotFound(QuotaNotFound):
message = _("Quota for project %(project_id)s could not be found.") message = _("Quota for project %(project_id)s could not be found.")
@ -395,19 +400,27 @@ class QuotaError(ManilaException):
class ShareSizeExceedsAvailableQuota(QuotaError): class ShareSizeExceedsAvailableQuota(QuotaError):
message = _("Requested share exceeds allowed gigabytes quota.") message = _(
"Requested share exceeds allowed project/user or share type "
"gigabytes quota.")
class SnapshotSizeExceedsAvailableQuota(QuotaError): class SnapshotSizeExceedsAvailableQuota(QuotaError):
message = _("Requested snapshot exceeds allowed gigabytes quota.") message = _(
"Requested snapshot exceeds allowed project/user or share type "
"gigabytes quota.")
class ShareLimitExceeded(QuotaError): class ShareLimitExceeded(QuotaError):
message = _("Maximum number of shares allowed (%(allowed)d) exceeded.") message = _(
"Maximum number of shares allowed (%(allowed)d) either per "
"project/user or share type quota is exceeded.")
class SnapshotLimitExceeded(QuotaError): class SnapshotLimitExceeded(QuotaError):
message = _("Maximum number of snapshots allowed (%(allowed)d) exceeded.") message = _(
"Maximum number of snapshots allowed (%(allowed)d) either per "
"project/user or share type quota is exceeded.")
class ShareNetworksLimitExceeded(QuotaError): class ShareNetworksLimitExceeded(QuotaError):

View File

@ -69,15 +69,6 @@ class DbQuotaDriver(object):
quota information. The default driver utilizes the local quota information. The default driver utilizes the local
database. database.
""" """
def get_by_project_and_user(self, context, project_id, user_id, resource):
"""Get a specific quota by project and user."""
return db.quota_get(context, project_id, user_id, resource)
def get_by_project(self, context, project_id, resource):
"""Get a specific quota by project."""
return db.quota_get(context, project_id, resource)
def get_by_class(self, context, quota_class, resource): def get_by_class(self, context, quota_class, resource):
"""Get a specific quota by quota class.""" """Get a specific quota by quota class."""
@ -96,7 +87,6 @@ class DbQuotaDriver(object):
for resource in resources.values(): for resource in resources.values():
quotas[resource.name] = default_quotas.get(resource.name, quotas[resource.name] = default_quotas.get(resource.name,
resource.default) resource.default)
return quotas return quotas
def get_class_quotas(self, context, resources, quota_class, def get_class_quotas(self, context, resources, quota_class,
@ -209,8 +199,7 @@ class DbQuotaDriver(object):
remains=remains) remains=remains)
def get_user_quotas(self, context, resources, project_id, user_id, def get_user_quotas(self, context, resources, project_id, user_id,
quota_class=None, defaults=True, quota_class=None, defaults=True, usages=True):
usages=True):
"""Retrieve quotas for user and project. """Retrieve quotas for user and project.
Given a list of resources, retrieve the quotas for the given Given a list of resources, retrieve the quotas for the given
@ -232,8 +221,8 @@ class DbQuotaDriver(object):
:param usages: If True, the current in_use and reserved counts :param usages: If True, the current in_use and reserved counts
will also be returned. will also be returned.
""" """
user_quotas = db.quota_get_all_by_project_and_user(context, user_quotas = db.quota_get_all_by_project_and_user(
project_id, user_id) context, project_id, user_id)
# Use the project quota for default user quota. # Use the project quota for default user quota.
proj_quotas = db.quota_get_all_by_project(context, project_id) proj_quotas = db.quota_get_all_by_project(context, project_id)
for key, value in proj_quotas.items(): for key, value in proj_quotas.items():
@ -247,8 +236,47 @@ class DbQuotaDriver(object):
user_quotas, quota_class, user_quotas, quota_class,
defaults=defaults, usages=user_usages) defaults=defaults, usages=user_usages)
def get_share_type_quotas(self, context, resources, project_id,
share_type_id, quota_class=None, defaults=True,
usages=True):
"""Retrieve quotas for share_type and project.
Given a list of resources, retrieve the quotas for the given
share_type and project.
:param context: The request context, for access checks.
:param resources: A dictionary of the registered resources.
:param project_id: The UUID of the project to return quotas for.
:param share_type: UUID/name of a share type to return quotas for.
:param quota_class: If project_id != context.project_id, the
quota class cannot be determined. This
parameter allows it to be specified. It
will be ignored if project_id ==
context.project_id.
:param defaults: If True, the quota class value (or the
default value, if there is no value from the
quota class) will be reported if there is no
specific value for the resource.
:param usages: If True, the current in_use and reserved counts
will also be returned.
"""
st_quotas = db.quota_get_all_by_project_and_share_type(
context, project_id, share_type_id)
# Use the project quota for default share_type quota.
project_quotas = db.quota_get_all_by_project(context, project_id)
for key, value in project_quotas.items():
if key not in st_quotas.keys():
st_quotas[key] = value
st_usages = None
if usages:
st_usages = db.quota_usage_get_all_by_project_and_share_type(
context, project_id, share_type_id)
return self._process_quotas(
context, resources, project_id, st_quotas, quota_class,
defaults=defaults, usages=st_usages)
def get_settable_quotas(self, context, resources, project_id, def get_settable_quotas(self, context, resources, project_id,
user_id=None): user_id=None, share_type_id=None):
"""Retrieve range of settable quotas. """Retrieve range of settable quotas.
Given a list of resources, retrieve the range of settable quotas for Given a list of resources, retrieve the range of settable quotas for
@ -258,30 +286,34 @@ class DbQuotaDriver(object):
:param resources: A dictionary of the registered resources. :param resources: A dictionary of the registered resources.
:param project_id: The ID of the project to return quotas for. :param project_id: The ID of the project to return quotas for.
:param user_id: The ID of the user to return quotas for. :param user_id: The ID of the user to return quotas for.
:param share_type_id: The UUID of the share_type to return quotas for.
""" """
settable_quotas = {} settable_quotas = {}
project_quotas = self.get_project_quotas(context, resources, project_quotas = self.get_project_quotas(
project_id, remains=True) context, resources, project_id, remains=True)
if user_id: if user_id or share_type_id:
user_quotas = self.get_user_quotas(context, resources, if user_id:
project_id, user_id) subquotas = self.get_user_quotas(
setted_quotas = db.quota_get_all_by_project_and_user( context, resources, project_id, user_id)
context, project_id, user_id) else:
for key, value in user_quotas.items(): subquotas = self.get_share_type_quotas(
maximum = (project_quotas[key]['remains'] + context, resources, project_id, share_type_id)
setted_quotas.get(key, 0)) for key, value in subquotas.items():
settable_quotas[key] = dict( settable_quotas[key] = {
minimum=value['in_use'] + value['reserved'], "minimum": value['in_use'] + value['reserved'],
maximum=maximum) "maximum": project_quotas[key]["limit"],
}
else: else:
for key, value in project_quotas.items(): for key, value in project_quotas.items():
minimum = max(int(value['limit'] - value['remains']), minimum = max(
int(value['in_use'] + value['reserved'])) int(value['limit'] - value['remains']),
settable_quotas[key] = dict(minimum=minimum, maximum=-1) int(value['in_use'] + value['reserved'])
)
settable_quotas[key] = {"minimum": minimum, "maximum": -1}
return settable_quotas return settable_quotas
def _get_quotas(self, context, resources, keys, has_sync, project_id=None, def _get_quotas(self, context, resources, keys, has_sync, project_id=None,
user_id=None): user_id=None, share_type_id=None):
"""Retrieve quotas for a resource. """Retrieve quotas for a resource.
A helper method which retrieves the quotas for the specific A helper method which retrieves the quotas for the specific
@ -323,6 +355,11 @@ class DbQuotaDriver(object):
quotas = self.get_user_quotas(context, sub_resources, quotas = self.get_user_quotas(context, sub_resources,
project_id, user_id, project_id, user_id,
context.quota_class, usages=False) context.quota_class, usages=False)
elif share_type_id:
# Grab and return the quotas (without usages)
quotas = self.get_share_type_quotas(
context, sub_resources, project_id, share_type_id,
context.quota_class, usages=False)
else: else:
# Grab and return the quotas (without usages) # Grab and return the quotas (without usages)
quotas = self.get_project_quotas(context, sub_resources, quotas = self.get_project_quotas(context, sub_resources,
@ -332,66 +369,8 @@ class DbQuotaDriver(object):
return {k: v['limit'] for k, v in quotas.items()} return {k: v['limit'] for k, v in quotas.items()}
def limit_check(self, context, resources, values, project_id=None,
user_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.
:param user_id: Specify the user_id if current context
is admin and admin wants to impact on
common user. (Special case: user operates on
resource, owned/created by different user)
"""
# 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
# If user id is None, then we use the user_id in context
if user_id is None:
user_id = context.user_id
# Get the applicable quotas
quotas = self._get_quotas(context, resources, values.keys(),
has_sync=False, project_id=project_id)
user_quotas = self._get_quotas(context, resources, values.keys(),
has_sync=False, project_id=project_id,
user_id=user_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) or
(user_quotas[key] >= 0 and user_quotas[key] < val)]
if overs:
raise exception.OverQuota(overs=sorted(overs), quotas=quotas,
usages={})
def reserve(self, context, resources, deltas, expire=None, def reserve(self, context, resources, deltas, expire=None,
project_id=None, user_id=None): project_id=None, user_id=None, share_type_id=None):
"""Check quotas and reserve resources. """Check quotas and reserve resources.
For counting quotas--those quotas for which there is a usage For counting quotas--those quotas for which there is a usage
@ -451,23 +430,31 @@ class DbQuotaDriver(object):
# NOTE(Vek): We're not worried about races at this point. # NOTE(Vek): We're not worried about races at this point.
# Yes, the admin may be in the process of reducing # Yes, the admin may be in the process of reducing
# quotas, but that's a pretty rare thing. # quotas, but that's a pretty rare thing.
quotas = self._get_quotas(context, resources, deltas.keys(), quotas = self._get_quotas(
has_sync=True, project_id=project_id) context, resources, deltas, has_sync=True, project_id=project_id)
user_quotas = self._get_quotas(context, resources, deltas.keys(), user_quotas = self._get_quotas(
has_sync=True, project_id=project_id, context, resources, deltas, has_sync=True,
user_id=user_id) project_id=project_id, user_id=user_id)
if share_type_id:
share_type_quotas = self._get_quotas(
context, resources, deltas, has_sync=True,
project_id=project_id, share_type_id=share_type_id)
else:
share_type_quotas = {}
# NOTE(Vek): Most of the work here has to be done in the DB # NOTE(Vek): Most of the work here has to be done in the DB
# API, because we have to do it in a transaction, # API, because we have to do it in a transaction,
# which means access to the session. Since the # which means access to the session. Since the
# session isn't available outside the DBAPI, we # session isn't available outside the DBAPI, we
# have to do the work there. # have to do the work there.
return db.quota_reserve(context, resources, quotas, user_quotas, return db.quota_reserve(
deltas, expire, context, resources, quotas, user_quotas, share_type_quotas,
CONF.until_refresh, CONF.max_age, deltas, expire, CONF.until_refresh, CONF.max_age,
project_id=project_id, user_id=user_id) project_id=project_id, user_id=user_id,
share_type_id=share_type_id)
def commit(self, context, reservations, project_id=None, user_id=None): def commit(self, context, reservations, project_id=None, user_id=None,
share_type_id=None):
"""Commit reservations. """Commit reservations.
:param context: The request context, for access checks. :param context: The request context, for access checks.
@ -488,10 +475,12 @@ class DbQuotaDriver(object):
if user_id is None: if user_id is None:
user_id = context.user_id user_id = context.user_id
db.reservation_commit(context, reservations, project_id=project_id, db.reservation_commit(
user_id=user_id) context, reservations, project_id=project_id, user_id=user_id,
share_type_id=share_type_id)
def rollback(self, context, reservations, project_id=None, user_id=None): def rollback(self, context, reservations, project_id=None, user_id=None,
share_type_id=None):
"""Roll back reservations. """Roll back reservations.
:param context: The request context, for access checks. :param context: The request context, for access checks.
@ -512,8 +501,9 @@ class DbQuotaDriver(object):
if user_id is None: if user_id is None:
user_id = context.user_id user_id = context.user_id
db.reservation_rollback(context, reservations, project_id=project_id, db.reservation_rollback(
user_id=user_id) context, reservations, project_id=project_id, user_id=user_id,
share_type_id=share_type_id)
def usage_reset(self, context, resources): def usage_reset(self, context, resources):
"""Reset usage records. """Reset usage records.
@ -571,6 +561,21 @@ class DbQuotaDriver(object):
db.quota_destroy_all_by_project_and_user(context, project_id, user_id) db.quota_destroy_all_by_project_and_user(context, project_id, user_id)
def destroy_all_by_project_and_share_type(self, context, project_id,
share_type_id):
"""Destroy metadata associated with a project and share_type.
Destroy all quotas, usages, and reservations associated with a
project and share_type.
:param context: The request context, for access checks.
:param project_id: The ID of the project.
:param share_type_id: The UUID of the share type.
"""
db.quota_destroy_all_by_project_and_share_type(
context, project_id, share_type_id)
def expire(self, context): def expire(self, context):
"""Expire reservations. """Expire reservations.
@ -598,54 +603,6 @@ class BaseResource(object):
self.name = name self.name = name
self.flag = flag self.flag = flag
def quota(self, driver, context, **kwargs):
"""Obtain quota for a resource.
Given a driver and context, obtain the quota for this
resource.
:param driver: A quota driver.
:param context: The request context.
:param project_id: The project to obtain the quota value for.
If not provided, it is taken from the
context. If it is given as None, no
project-specific quota will be searched
for.
:param quota_class: The quota class corresponding to the
project, or for which the quota is to be
looked up. If not provided, it is taken
from the context. If it is given as None,
no quota class-specific quota will be
searched for. Note that the quota class
defaults to the value in the context,
which may not correspond to the project if
project_id is not the same as the one in
the context.
"""
# Get the project ID
project_id = kwargs.get('project_id', context.project_id)
# Ditto for the quota class
quota_class = kwargs.get('quota_class', context.quota_class)
# Look up the quota for the project
if project_id:
try:
return driver.get_by_project(context, project_id, self.name)
except exception.ProjectQuotaNotFound:
pass
# Try for the quota class
if quota_class:
try:
return driver.get_by_class(context, quota_class, self.name)
except exception.QuotaClassNotFound:
pass
# OK, return the default
return self.default
@property @property
def default(self): def default(self):
"""Return the default value of the quota.""" """Return the default value of the quota."""
@ -768,17 +725,6 @@ class QuotaEngine(object):
for resource in resources: for resource in resources:
self.register_resource(resource) self.register_resource(resource)
def get_by_project_and_user(self, context, project_id, user_id, resource):
"""Get a specific quota by project and user."""
return self._driver.get_by_project_and_user(context, project_id,
user_id, resource)
def get_by_project(self, context, project_id, resource):
"""Get a specific quota by project."""
return self._driver.get_by_project(context, project_id, resource)
def get_by_class(self, context, quota_class, resource): def get_by_class(self, context, quota_class, resource):
"""Get a specific quota by quota class.""" """Get a specific quota by quota class."""
@ -830,6 +776,28 @@ class QuotaEngine(object):
defaults=defaults, defaults=defaults,
usages=usages) usages=usages)
def get_share_type_quotas(self, context, project_id, share_type_id,
quota_class=None, defaults=True, usages=True):
"""Retrieve the quotas for the given user and project.
:param context: The request context, for access checks.
:param project_id: The ID of the project to return quotas for.
:param share_type_id: The UUID of the user to return quotas for.
:param quota_class: If project_id != context.project_id, the
quota class cannot be determined. This
parameter allows it to be specified.
:param defaults: If True, the quota class value (or the
default value, if there is no value from the
quota class) will be reported if there is no
specific value for the resource.
:param usages: If True, the current in_use and reserved counts
will also be returned.
"""
return self._driver.get_share_type_quotas(
context, self._resources, project_id, share_type_id,
quota_class=quota_class, defaults=defaults, usages=usages)
def get_project_quotas(self, context, project_id, quota_class=None, def get_project_quotas(self, context, project_id, quota_class=None,
defaults=True, usages=True, remains=False): defaults=True, usages=True, remains=False):
"""Retrieve the quotas for the given project. """Retrieve the quotas for the given project.
@ -856,7 +824,8 @@ class QuotaEngine(object):
usages=usages, usages=usages,
remains=remains) remains=remains)
def get_settable_quotas(self, context, project_id, user_id=None): def get_settable_quotas(self, context, project_id, user_id=None,
share_type_id=None):
"""Get settable quotas. """Get settable quotas.
Given a list of resources, retrieve the range of settable quotas for Given a list of resources, retrieve the range of settable quotas for
@ -866,11 +835,12 @@ class QuotaEngine(object):
:param resources: A dictionary of the registered resources. :param resources: A dictionary of the registered resources.
:param project_id: The ID of the project to return quotas for. :param project_id: The ID of the project to return quotas for.
:param user_id: The ID of the user to return quotas for. :param user_id: The ID of the user to return quotas for.
:param share_type_id: The UUID of the share_type to return quotas for.
""" """
return self._driver.get_settable_quotas(context, self._resources, return self._driver.get_settable_quotas(
project_id, context, self._resources, project_id, user_id=user_id,
user_id=user_id) share_type_id=share_type_id)
def count(self, context, resource, *args, **kwargs): def count(self, context, resource, *args, **kwargs):
"""Count a resource. """Count a resource.
@ -891,40 +861,8 @@ class QuotaEngine(object):
return res.count(context, *args, **kwargs) return res.count(context, *args, **kwargs)
def limit_check(self, context, project_id=None, user_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.
:param user_id: Specify the user_id if current context
is admin and admin wants to impact on
common user. (Special case: user operates on
resource, owned/created by different user)
"""
return self._driver.limit_check(context, self._resources, values,
project_id=project_id, user_id=user_id)
def reserve(self, context, expire=None, project_id=None, user_id=None, def reserve(self, context, expire=None, project_id=None, user_id=None,
**deltas): share_type_id=None, **deltas):
"""Check quotas and reserve resources. """Check quotas and reserve resources.
For counting quotas--those quotas for which there is a usage For counting quotas--those quotas for which there is a usage
@ -959,16 +897,20 @@ class QuotaEngine(object):
common user's tenant. common user's tenant.
""" """
reservations = self._driver.reserve(context, self._resources, deltas, reservations = self._driver.reserve(
expire=expire, context, self._resources, deltas,
project_id=project_id, expire=expire,
user_id=user_id) project_id=project_id,
user_id=user_id,
share_type_id=share_type_id,
)
LOG.debug("Created reservations %s", reservations) LOG.debug("Created reservations %s", reservations)
return reservations return reservations
def commit(self, context, reservations, project_id=None, user_id=None): def commit(self, context, reservations, project_id=None, user_id=None,
share_type_id=None):
"""Commit reservations. """Commit reservations.
:param context: The request context, for access checks. :param context: The request context, for access checks.
@ -980,8 +922,9 @@ class QuotaEngine(object):
""" """
try: try:
self._driver.commit(context, reservations, project_id=project_id, self._driver.commit(
user_id=user_id) context, reservations, project_id=project_id,
user_id=user_id, share_type_id=share_type_id)
except Exception: except Exception:
# NOTE(Vek): Ignoring exceptions here is safe, because the # NOTE(Vek): Ignoring exceptions here is safe, because the
# usage resynchronization and the reservation expiration # usage resynchronization and the reservation expiration
@ -992,7 +935,8 @@ class QuotaEngine(object):
return return
LOG.debug("Committed reservations %s", reservations) LOG.debug("Committed reservations %s", reservations)
def rollback(self, context, reservations, project_id=None, user_id=None): def rollback(self, context, reservations, project_id=None, user_id=None,
share_type_id=None):
"""Roll back reservations. """Roll back reservations.
:param context: The request context, for access checks. :param context: The request context, for access checks.
@ -1004,8 +948,9 @@ class QuotaEngine(object):
""" """
try: try:
self._driver.rollback(context, reservations, project_id=project_id, self._driver.rollback(
user_id=user_id) context, reservations, project_id=project_id,
user_id=user_id, share_type_id=share_type_id)
except Exception: except Exception:
# NOTE(Vek): Ignoring exceptions here is safe, because the # NOTE(Vek): Ignoring exceptions here is safe, because the
# usage resynchronization and the reservation expiration # usage resynchronization and the reservation expiration
@ -1048,6 +993,21 @@ class QuotaEngine(object):
self._driver.destroy_all_by_project_and_user(context, self._driver.destroy_all_by_project_and_user(context,
project_id, user_id) project_id, user_id)
def destroy_all_by_project_and_share_type(self, context, project_id,
share_type_id):
"""Destroy metadata associated with a project and share_type.
Destroy all quotas, usages, and reservations associated with a
project and share_type.
:param context: The request context, for access checks.
:param project_id: The ID of the project.
:param share_type_id: The UUID of the share_type.
"""
self._driver.destroy_all_by_project_and_share_type(
context, project_id, share_type_id)
def destroy_all_by_project(self, context, project_id): def destroy_all_by_project(self, context, project_id):
"""Destroy metadata associated with a project. """Destroy metadata associated with a project.

View File

@ -138,7 +138,10 @@ class API(base.Base):
raise exception.InvalidInput(reason=msg) raise exception.InvalidInput(reason=msg)
try: try:
reservations = QUOTAS.reserve(context, shares=1, gigabytes=size) reservations = QUOTAS.reserve(
context, shares=1, gigabytes=size,
share_type_id=share_type_id,
)
except exception.OverQuota as e: except exception.OverQuota as e:
overs = e.kwargs['overs'] overs = e.kwargs['overs']
usages = e.kwargs['usages'] usages = e.kwargs['usages']
@ -233,13 +236,14 @@ class API(base.Base):
try: try:
share = self.db.share_create(context, options, share = self.db.share_create(context, options,
create_share_instance=False) create_share_instance=False)
QUOTAS.commit(context, reservations) QUOTAS.commit(context, reservations, share_type_id=share_type_id)
except Exception: except Exception:
with excutils.save_and_reraise_exception(): with excutils.save_and_reraise_exception():
try: try:
self.db.share_delete(context, share['id']) self.db.share_delete(context, share['id'])
finally: finally:
QUOTAS.rollback(context, reservations) QUOTAS.rollback(
context, reservations, share_type_id=share_type_id)
host = None host = None
if snapshot and not CONF.use_scheduler_creating_share_from_snapshot: if snapshot and not CONF.use_scheduler_creating_share_from_snapshot:
@ -778,7 +782,9 @@ class API(base.Base):
except Exception: except Exception:
with excutils.save_and_reraise_exception(): with excutils.save_and_reraise_exception():
if reservations: if reservations:
QUOTAS.rollback(context, reservations) QUOTAS.rollback(
context, reservations,
share_type_id=share['instance']['share_type_id'])
def _handle_revert_to_snapshot_quotas(self, context, share, snapshot): def _handle_revert_to_snapshot_quotas(self, context, share, snapshot):
"""Reserve extra quota if a revert will result in a larger share.""" """Reserve extra quota if a revert will result in a larger share."""
@ -789,10 +795,12 @@ class API(base.Base):
return None return None
try: try:
return QUOTAS.reserve(context, return QUOTAS.reserve(
project_id=share['project_id'], context,
gigabytes=size_increase, project_id=share['project_id'],
user_id=share['user_id']) gigabytes=size_increase,
user_id=share['user_id'],
share_type_id=share['instance']['share_type_id'])
except exception.OverQuota as exc: except exception.OverQuota as exc:
usages = exc.kwargs['usages'] usages = exc.kwargs['usages']
quotas = exc.kwargs['quotas'] quotas = exc.kwargs['quotas']
@ -919,11 +927,13 @@ class API(base.Base):
try: try:
# we give the user_id of the share, to update the quota usage # we give the user_id of the share, to update the quota usage
# for the user, who created the share # for the user, who created the share
reservations = QUOTAS.reserve(context, reservations = QUOTAS.reserve(
project_id=project_id, context,
shares=-1, project_id=project_id,
gigabytes=-share['size'], shares=-1,
user_id=share['user_id']) gigabytes=-share['size'],
user_id=share['user_id'],
share_type_id=share['instance']['share_type_id'])
except Exception as e: except Exception as e:
reservations = None reservations = None
LOG.exception( LOG.exception(
@ -938,8 +948,11 @@ class API(base.Base):
if reservations: if reservations:
# we give the user_id of the share, to update the quota usage # we give the user_id of the share, to update the quota usage
# for the user, who created the share # for the user, who created the share
QUOTAS.commit(context, reservations, project_id=project_id, QUOTAS.commit(
user_id=share['user_id']) context, reservations, project_id=project_id,
user_id=share['user_id'],
share_type_id=share['instance']['share_type_id'],
)
def delete_instance(self, context, share_instance, force=False): def delete_instance(self, context, share_instance, force=False):
policy.check_policy(context, 'share', 'delete') policy.check_policy(context, 'share', 'delete')
@ -1010,7 +1023,8 @@ class API(base.Base):
try: try:
reservations = QUOTAS.reserve( reservations = QUOTAS.reserve(
context, snapshots=1, snapshot_gigabytes=size) context, snapshots=1, snapshot_gigabytes=size,
share_type_id=share['instance']['share_type_id'])
except exception.OverQuota as e: except exception.OverQuota as e:
overs = e.kwargs['overs'] overs = e.kwargs['overs']
usages = e.kwargs['usages'] usages = e.kwargs['usages']
@ -1049,13 +1063,17 @@ class API(base.Base):
try: try:
snapshot = self.db.share_snapshot_create(context, options) snapshot = self.db.share_snapshot_create(context, options)
QUOTAS.commit(context, reservations) QUOTAS.commit(
context, reservations,
share_type_id=share['instance']['share_type_id'])
except Exception: except Exception:
with excutils.save_and_reraise_exception(): with excutils.save_and_reraise_exception():
try: try:
self.db.snapshot_delete(context, share['id']) self.db.snapshot_delete(context, share['id'])
finally: finally:
QUOTAS.rollback(context, reservations) QUOTAS.rollback(
context, reservations,
share_type_id=share['instance']['share_type_id'])
# If replicated share, create snapshot instances for each replica # If replicated share, create snapshot instances for each replica
if share.get('has_replicas'): if share.get('has_replicas'):
@ -1793,10 +1811,12 @@ class API(base.Base):
# we give the user_id of the share, to update the quota usage # we give the user_id of the share, to update the quota usage
# for the user, who created the share, because on share delete # for the user, who created the share, because on share delete
# only this quota will be decreased # only this quota will be decreased
reservations = QUOTAS.reserve(context, reservations = QUOTAS.reserve(
project_id=share['project_id'], context,
gigabytes=size_increase, project_id=share['project_id'],
user_id=share['user_id']) gigabytes=size_increase,
user_id=share['user_id'],
share_type_id=share['instance']['share_type_id'])
except exception.OverQuota as exc: except exception.OverQuota as exc:
usages = exc.kwargs['usages'] usages = exc.kwargs['usages']
quotas = exc.kwargs['quotas'] quotas = exc.kwargs['quotas']

View File

@ -2199,12 +2199,18 @@ class ShareManager(manager.SchedulerDependentManager):
msg = _("Driver cannot calculate share size.") msg = _("Driver cannot calculate share size.")
raise exception.InvalidShare(reason=msg) raise exception.InvalidShare(reason=msg)
reservations = QUOTAS.reserve(context, reservations = QUOTAS.reserve(
project_id=project_id, context,
user_id=context.user_id, project_id=project_id,
shares=1, user_id=context.user_id,
gigabytes=share_update['size']) shares=1,
QUOTAS.commit(context, reservations, project_id=project_id) gigabytes=share_update['size'],
share_type_id=share_instance['share_type_id'],
)
QUOTAS.commit(
context, reservations, project_id=project_id,
share_type_id=share_instance['share_type_id'],
)
share_update.update({ share_update.update({
'status': constants.STATUS_AVAILABLE, 'status': constants.STATUS_AVAILABLE,
@ -2371,11 +2377,17 @@ class ShareManager(manager.SchedulerDependentManager):
return return
try: try:
reservations = QUOTAS.reserve(context, reservations = QUOTAS.reserve(
project_id=project_id, context,
shares=-1, project_id=project_id,
gigabytes=-share_ref['size']) shares=-1,
QUOTAS.commit(context, reservations, project_id=project_id) gigabytes=-share_ref['size'],
share_type_id=share_instance['share_type_id'],
)
QUOTAS.commit(
context, reservations, project_id=project_id,
share_type_id=share_instance['share_type_id'],
)
except Exception as e: except Exception as e:
# Note(imalinovskiy): # Note(imalinovskiy):
# Quota reservation errors here are not fatal, because # Quota reservation errors here are not fatal, because
@ -2452,12 +2464,18 @@ class ShareManager(manager.SchedulerDependentManager):
return return
try: try:
share_type_id = snapshot_ref['share']['instance']['share_type_id']
reservations = QUOTAS.reserve( reservations = QUOTAS.reserve(
context, context,
project_id=project_id, project_id=project_id,
snapshots=-1, snapshots=-1,
snapshot_gigabytes=-snapshot_ref['size']) snapshot_gigabytes=-snapshot_ref['size'],
QUOTAS.commit(context, reservations, project_id=project_id) share_type_id=share_type_id,
)
QUOTAS.commit(
context, reservations, project_id=project_id,
share_type_id=share_type_id,
)
except Exception as e: except Exception as e:
# Note(imalinovskiy): # Note(imalinovskiy):
# Quota reservation errors here are not fatal, because # Quota reservation errors here are not fatal, because
@ -2500,6 +2518,7 @@ class ShareManager(manager.SchedulerDependentManager):
snapshot_instance = self.db.share_snapshot_instance_get( snapshot_instance = self.db.share_snapshot_instance_get(
context, snapshot.instance['id'], with_share_data=True) context, snapshot.instance['id'], with_share_data=True)
share_type_id = snapshot_instance["share_instance"]["share_type_id"]
# Make primitive to pass the information to the driver # Make primitive to pass the information to the driver
snapshot_instance_dict = self._get_snapshot_instance_dict( snapshot_instance_dict = self._get_snapshot_instance_dict(
@ -2521,7 +2540,8 @@ class ShareManager(manager.SchedulerDependentManager):
if reservations: if reservations:
QUOTAS.rollback( QUOTAS.rollback(
context, reservations, project_id=project_id, context, reservations, project_id=project_id,
user_id=user_id) user_id=user_id, share_type_id=share_type_id,
)
self.db.share_update( self.db.share_update(
context, share_id, context, share_id,
@ -2532,7 +2552,9 @@ class ShareManager(manager.SchedulerDependentManager):
if reservations: if reservations:
QUOTAS.commit( QUOTAS.commit(
context, reservations, project_id=project_id, user_id=user_id) context, reservations, project_id=project_id, user_id=user_id,
share_type_id=share_type_id,
)
self.db.share_update( self.db.share_update(
context, share_id, context, share_id,
@ -2743,19 +2765,25 @@ class ShareManager(manager.SchedulerDependentManager):
self.db.share_snapshot_instance_delete(context, snapshot_instance_id) self.db.share_snapshot_instance_delete(context, snapshot_instance_id)
share_type_id = snapshot_ref['share']['instance']['share_type_id']
try: try:
reservations = QUOTAS.reserve( reservations = QUOTAS.reserve(
context, project_id=project_id, snapshots=-1, context, project_id=project_id, snapshots=-1,
snapshot_gigabytes=-snapshot_ref['size'], snapshot_gigabytes=-snapshot_ref['size'],
user_id=snapshot_ref['user_id']) user_id=snapshot_ref['user_id'],
share_type_id=share_type_id,
)
except Exception: except Exception:
reservations = None reservations = None
LOG.exception("Failed to update quota usages while deleting " LOG.exception("Failed to update quota usages while deleting "
"snapshot %s.", snapshot_id) "snapshot %s.", snapshot_id)
if reservations: if reservations:
QUOTAS.commit(context, reservations, project_id=project_id, QUOTAS.commit(
user_id=snapshot_ref['user_id']) context, reservations, project_id=project_id,
user_id=snapshot_ref['user_id'],
share_type_id=share_type_id,
)
@add_hooks @add_hooks
@utils.require_driver_initialized @utils.require_driver_initialized
@ -2860,7 +2888,9 @@ class ShareManager(manager.SchedulerDependentManager):
if reservations: if reservations:
QUOTAS.rollback( QUOTAS.rollback(
context, reservations, project_id=project_id, context, reservations, project_id=project_id,
user_id=user_id) user_id=user_id,
share_type_id=active_replica['share_type_id'],
)
self.db.share_replica_update( self.db.share_replica_update(
context, active_replica['id'], context, active_replica['id'],
@ -2871,7 +2901,9 @@ class ShareManager(manager.SchedulerDependentManager):
if reservations: if reservations:
QUOTAS.commit( QUOTAS.commit(
context, reservations, project_id=project_id, user_id=user_id) context, reservations, project_id=project_id, user_id=user_id,
share_type_id=active_replica['share_type_id'],
)
self.db.share_update(context, share_id, {'size': snapshot['size']}) self.db.share_update(context, share_id, {'size': snapshot['size']})
self.db.share_replica_update( self.db.share_replica_update(
@ -3368,14 +3400,19 @@ class ShareManager(manager.SchedulerDependentManager):
raise exception.ShareExtendingError( raise exception.ShareExtendingError(
reason=six.text_type(e), share_id=share_id) reason=six.text_type(e), share_id=share_id)
finally: finally:
QUOTAS.rollback(context, reservations, project_id=project_id, QUOTAS.rollback(
user_id=user_id) context, reservations, project_id=project_id,
user_id=user_id,
share_type_id=share_instance['share_type_id'],
)
# we give the user_id of the share, to update the quota usage # we give the user_id of the share, to update the quota usage
# for the user, who created the share, because on share delete # for the user, who created the share, because on share delete
# only this quota will be decreased # only this quota will be decreased
QUOTAS.commit(context, reservations, project_id=project_id, QUOTAS.commit(
user_id=user_id) context, reservations, project_id=project_id,
user_id=user_id, share_type_id=share_instance['share_type_id'],
)
share_update = { share_update = {
'size': int(new_size), 'size': int(new_size),
@ -3418,10 +3455,13 @@ class ShareManager(manager.SchedulerDependentManager):
# we give the user_id of the share, to update the quota usage # we give the user_id of the share, to update the quota usage
# for the user, who created the share, because on share delete # for the user, who created the share, because on share delete
# only this quota will be decreased # only this quota will be decreased
reservations = QUOTAS.reserve(context, reservations = QUOTAS.reserve(
project_id=project_id, context,
user_id=user_id, project_id=project_id,
gigabytes=-size_decrease) user_id=user_id,
share_type_id=share_instance['share_type_id'],
gigabytes=-size_decrease,
)
except Exception as e: except Exception as e:
error_occurred( error_occurred(
e, ("Failed to update quota on share shrinking.")) e, ("Failed to update quota on share shrinking."))
@ -3443,11 +3483,16 @@ class ShareManager(manager.SchedulerDependentManager):
try: try:
error_occurred(e, **error_params) error_occurred(e, **error_params)
finally: finally:
QUOTAS.rollback(context, reservations, project_id=project_id, QUOTAS.rollback(
user_id=user_id) context, reservations, project_id=project_id,
user_id=user_id,
share_type_id=share_instance['share_type_id'],
)
QUOTAS.commit(context, reservations, project_id=project_id, QUOTAS.commit(
user_id=user_id) context, reservations, project_id=project_id,
user_id=user_id, share_type_id=share_instance['share_type_id'],
)
share_update = { share_update = {
'size': new_size, 'size': new_size,

View File

@ -15,7 +15,7 @@
# under the License. # under the License.
""" """
Tests for manila.api.v1.quota_sets.py Tests for manila.api.v2.quota_sets.py
""" """
import copy import copy
@ -37,7 +37,7 @@ from manila import utils
CONF = cfg.CONF CONF = cfg.CONF
REQ = mock.MagicMock() REQ = mock.MagicMock(api_version_request=api_version.APIVersionRequest("2.39"))
REQ.environ = {'manila.context': context.get_admin_context()} REQ.environ = {'manila.context': context.get_admin_context()}
REQ.environ['manila.context'].is_admin = True REQ.environ['manila.context'].is_admin = True
REQ.environ['manila.context'].auth_token = 'foo_auth_token' REQ.environ['manila.context'].auth_token = 'foo_auth_token'
@ -122,6 +122,160 @@ class QuotaSetsControllerTest(test.TestCase):
self.assertEqual(expected, result) self.assertEqual(expected, result)
@staticmethod
def _get_share_type_request_object(microversion=None):
req = copy.deepcopy(REQ)
req.environ['QUERY_STRING'] = 'share_type=fake_share_type_name_or_id'
req.api_version_request = api_version.APIVersionRequest(
microversion or '2.39')
return req
def test_share_type_quota_detail(self):
self.mock_object(
quota_sets.db, 'share_type_get_by_name_or_id',
mock.Mock(return_value={'id': 'fake_st_id'}))
req = self._get_share_type_request_object('2.39')
quotas = {
"shares": 23,
"snapshots": 34,
"gigabytes": 45,
"snapshot_gigabytes": 56,
}
expected = {'quota_set': {
'id': self.project_id,
'shares': {
'in_use': 0,
'limit': quotas['shares'],
'reserved': 0,
},
'gigabytes': {
'in_use': 0,
'limit': quotas['gigabytes'],
'reserved': 0,
},
'snapshots': {
'in_use': 0,
'limit': quotas['snapshots'],
'reserved': 0,
},
'snapshot_gigabytes': {
'in_use': 0,
'limit': quotas['snapshot_gigabytes'],
'reserved': 0,
},
}}
for k, v in quotas.items():
CONF.set_default('quota_' + k, v)
result = self.controller.detail(req, self.project_id)
self.assertEqual(expected, result)
self.mock_policy_check.assert_called_once_with(
req.environ['manila.context'], self.resource_name, 'show')
quota_sets.db.share_type_get_by_name_or_id.assert_called_once_with(
req.environ['manila.context'], 'fake_share_type_name_or_id')
def test_show_share_type_quota(self):
self.mock_object(
quota_sets.db, 'share_type_get_by_name_or_id',
mock.Mock(return_value={'id': 'fake_st_id'}))
req = self._get_share_type_request_object('2.39')
quotas = {
"shares": 23,
"snapshots": 34,
"gigabytes": 45,
"snapshot_gigabytes": 56,
}
expected = {
'quota_set': {
'id': self.project_id,
'shares': quotas.get('shares', 50),
'gigabytes': quotas.get('gigabytes', 1000),
'snapshots': quotas.get('snapshots', 50),
'snapshot_gigabytes': quotas.get('snapshot_gigabytes', 1000),
}
}
for k, v in quotas.items():
CONF.set_default('quota_' + k, v)
result = self.controller.show(req, self.project_id)
self.assertEqual(expected, result)
self.mock_policy_check.assert_called_once_with(
req.environ['manila.context'], self.resource_name, 'show')
quota_sets.db.share_type_get_by_name_or_id.assert_called_once_with(
req.environ['manila.context'], 'fake_share_type_name_or_id')
@ddt.data('show', 'detail')
def test_get_share_type_quota_with_old_microversion(self, method):
req = self._get_share_type_request_object('2.38')
self.assertRaises(
webob.exc.HTTPBadRequest,
getattr(self.controller, method),
req, self.project_id)
@ddt.data((None, None), (None, 'foo'), ('bar', None))
@ddt.unpack
def test__validate_user_id_and_share_type_args(self, user_id, st_id):
result = self.controller._validate_user_id_and_share_type_args(
user_id, st_id)
self.assertIsNone(result)
def test__validate_user_id_and_share_type_args_exception(self):
self.assertRaises(
webob.exc.HTTPBadRequest,
self.controller._validate_user_id_and_share_type_args,
'foo', 'bar')
def test__get_share_type_id_found(self):
self.mock_object(
quota_sets.db, 'share_type_get_by_name_or_id',
mock.Mock(return_value={'id': 'fake_st_id'}))
ctxt = 'fake_context'
share_type = 'fake_share_type_name_or_id'
result = self.controller._get_share_type_id(ctxt, share_type)
self.assertEqual('fake_st_id', result)
def test__get_share_type_id_not_found(self):
self.mock_object(
quota_sets.db, 'share_type_get_by_name_or_id',
mock.Mock(return_value=None))
ctxt = 'fake_context'
share_type = 'fake_share_type_name_or_id'
self.assertRaises(
webob.exc.HTTPNotFound,
self.controller._get_share_type_id,
ctxt, share_type)
def test__get_share_type_id_is_not_provided(self):
self.mock_object(
quota_sets.db, 'share_type_get_by_name_or_id',
mock.Mock(return_value={'id': 'fake_st_id'}))
ctxt = 'fake_context'
result = self.controller._get_share_type_id(ctxt, None)
self.assertIsNone(result)
@ddt.data(REQ, REQ_WITH_USER)
def test__ensure_share_type_arg_is_absent(self, req):
result = self.controller._ensure_share_type_arg_is_absent(req)
self.assertIsNone(result)
def test__ensure_share_type_arg_is_absent_exception(self):
req = self._get_share_type_request_object('2.39')
self.assertRaises(
webob.exc.HTTPBadRequest,
self.controller._ensure_share_type_arg_is_absent,
req)
@ddt.data(REQ, REQ_WITH_USER) @ddt.data(REQ, REQ_WITH_USER)
def test_quota_detail(self, request): def test_quota_detail(self, request):
request.api_version_request = api_version.APIVersionRequest('2.25') request.api_version_request = api_version.APIVersionRequest('2.25')
@ -207,6 +361,10 @@ class QuotaSetsControllerTest(test.TestCase):
@ddt.data(REQ, REQ_WITH_USER) @ddt.data(REQ, REQ_WITH_USER)
def test_update_quota(self, request): def test_update_quota(self, request):
self.mock_object(
quota_sets.db, 'share_type_get_by_name_or_id',
mock.Mock(
return_value={'id': 'fake_st_id', 'name': 'fake_st_name'}))
CONF.set_default('quota_shares', 789) CONF.set_default('quota_shares', 789)
body = {'quota_set': {'tenant_id': self.project_id, 'shares': 788}} body = {'quota_set': {'tenant_id': self.project_id, 'shares': 788}}
expected = { expected = {
@ -234,6 +392,79 @@ class QuotaSetsControllerTest(test.TestCase):
self.assertEqual(expected, show_result) self.assertEqual(expected, show_result)
self.mock_policy_check.assert_has_calls([ self.mock_policy_check.assert_has_calls([
mock_policy_update_check_call, mock_policy_show_check_call]) mock_policy_update_check_call, mock_policy_show_check_call])
quota_sets.db.share_type_get_by_name_or_id.assert_not_called()
def test_update_share_type_quota(self):
self.mock_object(
quota_sets.db, 'share_type_get_by_name_or_id',
mock.Mock(
return_value={'id': 'fake_st_id', 'name': 'fake_st_name'}))
req = self._get_share_type_request_object('2.39')
CONF.set_default('quota_shares', 789)
body = {'quota_set': {'tenant_id': self.project_id, 'shares': 788}}
expected = {
'quota_set': {
'shares': body['quota_set']['shares'],
'gigabytes': 1000,
'snapshots': 50,
'snapshot_gigabytes': 1000,
}
}
update_result = self.controller.update(req, self.project_id, body=body)
self.assertEqual(expected, update_result)
quota_sets.db.share_type_get_by_name_or_id.assert_called_once_with(
req.environ['manila.context'],
req.environ['QUERY_STRING'].split('=')[-1])
quota_sets.db.share_type_get_by_name_or_id.reset_mock()
show_result = self.controller.show(req, self.project_id)
expected['quota_set']['id'] = self.project_id
self.assertEqual(expected, show_result)
self.mock_policy_check.assert_has_calls([
mock.call(req.environ['manila.context'], self.resource_name, key)
for key in ('update', 'show')
])
quota_sets.db.share_type_get_by_name_or_id.assert_called_once_with(
req.environ['manila.context'],
req.environ['QUERY_STRING'].split('=')[-1])
def test_update_share_type_quota_using_too_old_microversion(self):
self.mock_object(
quota_sets.db, 'share_type_get_by_name_or_id',
mock.Mock(
return_value={'id': 'fake_st_id', 'name': 'fake_st_name'}))
req = self._get_share_type_request_object('2.38')
body = {'quota_set': {'tenant_id': self.project_id, 'shares': 788}}
self.assertRaises(
webob.exc.HTTPBadRequest,
self.controller.update,
req, self.project_id, body=body)
quota_sets.db.share_type_get_by_name_or_id.assert_not_called()
def test_update_share_type_quota_for_share_networks(self):
self.mock_object(
quota_sets.db, 'share_type_get_by_name_or_id',
mock.Mock(
return_value={'id': 'fake_st_id', 'name': 'fake_st_name'}))
req = self._get_share_type_request_object('2.39')
body = {'quota_set': {
'tenant_id': self.project_id, 'share_networks': 788,
}}
self.assertRaises(
webob.exc.HTTPBadRequest,
self.controller.update,
req, self.project_id, body=body)
quota_sets.db.share_type_get_by_name_or_id.assert_called_once_with(
req.environ['manila.context'],
req.environ['QUERY_STRING'].split('=')[-1])
@ddt.data(-2, 'foo', {1: 2}, [1]) @ddt.data(-2, 'foo', {1: 2}, [1])
def test_update_quota_with_invalid_value(self, value): def test_update_quota_with_invalid_value(self, value):
@ -384,6 +615,46 @@ class QuotaSetsControllerTest(test.TestCase):
REQ_WITH_USER.environ['manila.context'], self.resource_name, REQ_WITH_USER.environ['manila.context'], self.resource_name,
'delete') 'delete')
def test_delete_share_type_quota(self):
req = self._get_share_type_request_object('2.39')
self.mock_object(quota_sets.QUOTAS, 'destroy_all_by_project')
self.mock_object(quota_sets.QUOTAS, 'destroy_all_by_project_and_user')
mock_delete_st_quotas = self.mock_object(
quota_sets.QUOTAS, 'destroy_all_by_project_and_share_type')
self.mock_object(
quota_sets.db, 'share_type_get_by_name_or_id',
mock.Mock(
return_value={'id': 'fake_st_id', 'name': 'fake_st_name'}))
result = self.controller.delete(req, self.project_id)
self.assertEqual(utils.IsAMatcher(webob.response.Response), result)
self.assertTrue(hasattr(result, 'status_code'))
self.assertEqual(202, result.status_code)
mock_delete_st_quotas.assert_called_once_with(
req.environ['manila.context'], self.project_id, 'fake_st_id')
quota_sets.QUOTAS.destroy_all_by_project.assert_not_called()
quota_sets.QUOTAS.destroy_all_by_project_and_user.assert_not_called()
self.mock_policy_check.assert_called_once_with(
req.environ['manila.context'], self.resource_name, 'delete')
quota_sets.db.share_type_get_by_name_or_id.assert_called_once_with(
req.environ['manila.context'],
req.environ['QUERY_STRING'].split('=')[-1])
def test_delete_share_type_quota_using_too_old_microversion(self):
self.mock_object(
quota_sets.db, 'share_type_get_by_name_or_id',
mock.Mock(
return_value={'id': 'fake_st_id', 'name': 'fake_st_name'}))
req = self._get_share_type_request_object('2.38')
self.assertRaises(
webob.exc.HTTPBadRequest,
self.controller.delete,
req, self.project_id)
quota_sets.db.share_type_get_by_name_or_id.assert_not_called()
def test_delete_not_authorized(self): def test_delete_not_authorized(self):
self.assertRaises( self.assertRaises(
webob.exc.HTTPForbidden, webob.exc.HTTPForbidden,

View File

@ -2396,3 +2396,73 @@ class MessagesTableChecks(BaseMigrationChecks):
def check_downgrade(self, engine): def check_downgrade(self, engine):
self.test_case.assertRaises(sa_exc.NoSuchTableError, utils.load_table, self.test_case.assertRaises(sa_exc.NoSuchTableError, utils.load_table,
'messages', engine) 'messages', engine)
@map_to_migration('b516de97bfee')
class ProjectShareTypesQuotasChecks(BaseMigrationChecks):
new_table_name = 'project_share_type_quotas'
usages_table = 'quota_usages'
reservations_table = 'reservations'
st_record_id = uuidutils.generate_uuid()
def setup_upgrade_data(self, engine):
# Create share type
self.st_data = {
'id': self.st_record_id,
'name': uuidutils.generate_uuid(),
'deleted': "False",
}
st_table = utils.load_table('share_types', engine)
engine.execute(st_table.insert(self.st_data))
def check_upgrade(self, engine, data):
# Create share type quota
self.quota_data = {
'project_id': 'x' * 255,
'resource': 'y' * 255,
'hard_limit': 987654321,
'created_at': datetime.datetime(2017, 4, 11, 18, 5, 58),
'updated_at': None,
'deleted_at': None,
'deleted': 0,
'share_type_id': self.st_record_id,
}
new_table = utils.load_table(self.new_table_name, engine)
engine.execute(new_table.insert(self.quota_data))
# Create usage record
self.usages_data = {
'project_id': 'x' * 255,
'user_id': None,
'share_type_id': self.st_record_id,
'resource': 'y' * 255,
'in_use': 13,
'reserved': 15,
}
usages_table = utils.load_table(self.usages_table, engine)
engine.execute(usages_table.insert(self.usages_data))
# Create reservation record
self.reservations_data = {
'uuid': uuidutils.generate_uuid(),
'usage_id': 1,
'project_id': 'x' * 255,
'user_id': None,
'share_type_id': self.st_record_id,
'resource': 'y' * 255,
'delta': 13,
'expire': datetime.datetime(2399, 4, 11, 18, 5, 58),
}
reservations_table = utils.load_table(self.reservations_table, engine)
engine.execute(reservations_table.insert(self.reservations_data))
def check_downgrade(self, engine):
self.test_case.assertRaises(
sa_exc.NoSuchTableError,
utils.load_table, self.new_table_name, engine)
for table_name in (self.usages_table, self.reservations_table):
table = utils.load_table(table_name, engine)
db_result = engine.execute(table.select())
self.test_case.assertGreater(db_result.rowcount, 0)
for row in db_result:
self.test_case.assertFalse(hasattr(row, 'share_type_id'))

View File

@ -39,6 +39,7 @@ def fake_share(**kwargs):
'is_busy': False, 'is_busy': False,
'share_group_id': None, 'share_group_id': None,
'instance': { 'instance': {
'id': 'fake_share_instance_id',
'host': 'fakehost', 'host': 'fakehost',
'share_type_id': '1', 'share_type_id': '1',
}, },
@ -61,6 +62,7 @@ def fake_share_instance(base_share=None, **kwargs):
'host': 'fakehost', 'host': 'fakehost',
'share_network_id': 'fakesharenetworkid', 'share_network_id': 'fakesharenetworkid',
'share_server_id': 'fakeshareserverid', 'share_server_id': 'fakeshareserverid',
'share_type_id': '1',
} }
for attr in models.ShareInstance._proxified_properties: for attr in models.ShareInstance._proxified_properties:
@ -153,7 +155,10 @@ def fake_snapshot_instance(base_snapshot=None, as_primitive=False, **kwargs):
'provider_location': 'i_live_here_actually', 'provider_location': 'i_live_here_actually',
'share_name': 'fakename', 'share_name': 'fakename',
'share_id': 'fakeshareinstanceid', 'share_id': 'fakeshareinstanceid',
'share_instance': {'share_id': 'fakeshareid', }, 'share_instance': {
'share_id': 'fakeshareid',
'share_type_id': '1',
},
'share_instance_id': 'fakeshareinstanceid', 'share_instance_id': 'fakeshareinstanceid',
'deleted': False, 'deleted': False,
'updated_at': datetime.datetime(2016, 3, 21, 0, 5, 58), 'updated_at': datetime.datetime(2016, 3, 21, 0, 5, 58),

View File

@ -677,7 +677,8 @@ class ShareAPITestCase(test.TestCase):
share_data['display_description'] share_data['display_description']
) )
quota.QUOTAS.reserve.assert_called_once_with( quota.QUOTAS.reserve.assert_called_once_with(
self.context, shares=1, gigabytes=share_data['size']) self.context, share_type_id=None,
shares=1, gigabytes=share_data['size'])
@ddt.data(exception.QuotaError, exception.InvalidShare) @ddt.data(exception.QuotaError, exception.InvalidShare)
def test_create_share_error_on_quota_commit(self, expected_exception): def test_create_share_error_on_quota_commit(self, expected_exception):
@ -700,8 +701,8 @@ class ShareAPITestCase(test.TestCase):
share_data['display_description'] share_data['display_description']
) )
quota.QUOTAS.rollback.assert_called_once_with(self.context, quota.QUOTAS.rollback.assert_called_once_with(
reservation) self.context, reservation, share_type_id=None)
db_api.share_delete.assert_called_once_with(self.context, share['id']) db_api.share_delete.assert_called_once_with(self.context, share['id'])
def test_create_share_instance_with_host_and_az(self): def test_create_share_instance_with_host_and_az(self):
@ -1033,9 +1034,10 @@ class ShareAPITestCase(test.TestCase):
share_api.policy.check_policy.assert_called_once_with( share_api.policy.check_policy.assert_called_once_with(
self.context, 'share', 'create_snapshot', share) self.context, 'share', 'create_snapshot', share)
quota.QUOTAS.reserve.assert_called_once_with( quota.QUOTAS.reserve.assert_called_once_with(
self.context, snapshots=1, snapshot_gigabytes=1) self.context, share_type_id=None,
snapshot_gigabytes=1, snapshots=1)
quota.QUOTAS.commit.assert_called_once_with( quota.QUOTAS.commit.assert_called_once_with(
self.context, 'reservation') self.context, 'reservation', share_type_id=None)
db_api.share_snapshot_create.assert_called_once_with( db_api.share_snapshot_create.assert_called_once_with(
self.context, options) self.context, options)
@ -1248,7 +1250,8 @@ class ShareAPITestCase(test.TestCase):
if reservations is not None: if reservations is not None:
mock_quotas_rollback.assert_called_once_with( mock_quotas_rollback.assert_called_once_with(
self.context, reservations) self.context, reservations,
share_type_id=share['instance']['share_type_id'])
else: else:
self.assertFalse(mock_quotas_rollback.called) self.assertFalse(mock_quotas_rollback.called)
@ -1284,6 +1287,7 @@ class ShareAPITestCase(test.TestCase):
self.assertEqual('fake_reservations', result) self.assertEqual('fake_reservations', result)
mock_quotas_reserve.assert_called_once_with( mock_quotas_reserve.assert_called_once_with(
self.context, project_id='fake_project', gigabytes=1, self.context, project_id='fake_project', gigabytes=1,
share_type_id=share['instance']['share_type_id'],
user_id='fake_user') user_id='fake_user')
def test_handle_revert_to_snapshot_quotas_quota_exceeded(self): def test_handle_revert_to_snapshot_quotas_quota_exceeded(self):
@ -1649,9 +1653,10 @@ class ShareAPITestCase(test.TestCase):
mock.call(self.context, 'share', 'create'), mock.call(self.context, 'share', 'create'),
mock.call(self.context, 'share_snapshot', 'get_snapshot')]) mock.call(self.context, 'share_snapshot', 'get_snapshot')])
quota.QUOTAS.reserve.assert_called_once_with( quota.QUOTAS.reserve.assert_called_once_with(
self.context, gigabytes=1, shares=1) self.context, share_type_id=share_type['id'],
gigabytes=1, shares=1)
quota.QUOTAS.commit.assert_called_once_with( quota.QUOTAS.commit.assert_called_once_with(
self.context, 'reservation') self.context, 'reservation', share_type_id=share_type['id'])
def test_create_from_snapshot_with_different_share_type(self): def test_create_from_snapshot_with_different_share_type(self):
snapshot, share, share_data, request_spec = ( snapshot, share, share_data, request_spec = (
@ -1734,12 +1739,14 @@ class ShareAPITestCase(test.TestCase):
project_id=share['project_id'], project_id=share['project_id'],
shares=-1, shares=-1,
gigabytes=-share['size'], gigabytes=-share['size'],
share_type_id=None,
user_id=share['user_id'] user_id=share['user_id']
) )
quota.QUOTAS.commit.assert_called_once_with( quota.QUOTAS.commit.assert_called_once_with(
diff_user_context, diff_user_context,
mock.ANY, mock.ANY,
project_id=share['project_id'], project_id=share['project_id'],
share_type_id=None,
user_id=share['user_id'] user_id=share['user_id']
) )
@ -1810,6 +1817,7 @@ class ShareAPITestCase(test.TestCase):
project_id=share['project_id'], project_id=share['project_id'],
shares=-1, shares=-1,
gigabytes=-share['size'], gigabytes=-share['size'],
share_type_id=None,
user_id=share['user_id'] user_id=share['user_id']
) )
self.assertFalse(quota.QUOTAS.commit.called) self.assertFalse(quota.QUOTAS.commit.called)
@ -2239,6 +2247,7 @@ class ShareAPITestCase(test.TestCase):
diff_user_context, diff_user_context,
project_id=share['project_id'], project_id=share['project_id'],
gigabytes=size_increase, gigabytes=size_increase,
share_type_id=None,
user_id=share['user_id'] user_id=share['user_id']
) )

View File

@ -1606,7 +1606,7 @@ class ShareManagerTestCase(test.TestCase):
def test_delete_snapshot_with_quota_error(self, quota_error): def test_delete_snapshot_with_quota_error(self, quota_error):
share_id = 'FAKE_SHARE_ID' share_id = 'FAKE_SHARE_ID'
share = fakes.fake_share(id=share_id, instance={'id': 'fake_id'}) share = fakes.fake_share(id=share_id)
snapshot_instance = fakes.fake_snapshot_instance( snapshot_instance = fakes.fake_snapshot_instance(
share_id=share_id, share=share, name='fake_snapshot') share_id=share_id, share=share, name='fake_snapshot')
snapshot = fakes.fake_snapshot( snapshot = fakes.fake_snapshot(
@ -1648,7 +1648,8 @@ class ShareManagerTestCase(test.TestCase):
self.assertTrue(manager.QUOTAS.reserve.called) self.assertTrue(manager.QUOTAS.reserve.called)
quota.QUOTAS.reserve.assert_called_once_with( quota.QUOTAS.reserve.assert_called_once_with(
mock.ANY, project_id=self.context.project_id, snapshots=-1, mock.ANY, project_id=self.context.project_id, snapshots=-1,
snapshot_gigabytes=-snapshot['size'], user_id=snapshot['user_id']) snapshot_gigabytes=-snapshot['size'], user_id=snapshot['user_id'],
share_type_id=share['instance']['share_type_id'])
self.assertEqual(not quota_error, quota_commit_call.called) self.assertEqual(not quota_error, quota_commit_call.called)
self.assertEqual(quota_error, mock_exception_log.called) self.assertEqual(quota_error, mock_exception_log.called)
self.assertEqual(expected_exc_count, mock_exception_log.call_count) self.assertEqual(expected_exc_count, mock_exception_log.call_count)
@ -1660,7 +1661,7 @@ class ShareManagerTestCase(test.TestCase):
raise exception.QuotaError(code='500') raise exception.QuotaError(code='500')
share_id = 'FAKE_SHARE_ID' share_id = 'FAKE_SHARE_ID'
share = fakes.fake_share(id=share_id, instance={'id': 'fake_id'}) share = fakes.fake_share(id=share_id)
snapshot_instance = fakes.fake_snapshot_instance( snapshot_instance = fakes.fake_snapshot_instance(
share_id=share_id, share=share, name='fake_snapshot') share_id=share_id, share=share, name='fake_snapshot')
snapshot = fakes.fake_snapshot( snapshot = fakes.fake_snapshot(
@ -3038,7 +3039,8 @@ class ShareManagerTestCase(test.TestCase):
mock.ANY, mock.ANY,
reservations, reservations,
project_id=six.text_type(share['project_id']), project_id=six.text_type(share['project_id']),
user_id=six.text_type(share['user_id']) user_id=six.text_type(share['user_id']),
share_type_id=None,
) )
@mock.patch('manila.tests.fake_notifier.FakeNotifier._notify') @mock.patch('manila.tests.fake_notifier.FakeNotifier._notify')
@ -3076,7 +3078,7 @@ class ShareManagerTestCase(test.TestCase):
) )
quota.QUOTAS.commit.assert_called_once_with( quota.QUOTAS.commit.assert_called_once_with(
mock.ANY, reservations, project_id=share['project_id'], mock.ANY, reservations, project_id=share['project_id'],
user_id=share['user_id']) user_id=share['user_id'], share_type_id=None)
manager.db.share_update.assert_called_once_with( manager.db.share_update.assert_called_once_with(
mock.ANY, share_id, shr_update mock.ANY, share_id, shr_update
) )
@ -3103,6 +3105,7 @@ class ShareManagerTestCase(test.TestCase):
mock.ANY, mock.ANY,
project_id=six.text_type(share['project_id']), project_id=six.text_type(share['project_id']),
user_id=six.text_type(share['user_id']), user_id=six.text_type(share['user_id']),
share_type_id=None,
gigabytes=new_size - size gigabytes=new_size - size
) )
self.assertTrue(self.share_manager.db.share_update.called) self.assertTrue(self.share_manager.db.share_update.called)
@ -3140,11 +3143,11 @@ class ShareManagerTestCase(test.TestCase):
) )
quota.QUOTAS.reserve.assert_called_once_with( quota.QUOTAS.reserve.assert_called_once_with(
mock.ANY, gigabytes=-size_decrease, project_id=share['project_id'], mock.ANY, gigabytes=-size_decrease, project_id=share['project_id'],
user_id=share['user_id'] share_type_id=None, user_id=share['user_id'],
) )
quota.QUOTAS.rollback.assert_called_once_with( quota.QUOTAS.rollback.assert_called_once_with(
mock.ANY, mock.ANY, project_id=share['project_id'], mock.ANY, mock.ANY, project_id=share['project_id'],
user_id=share['user_id'] share_type_id=None, user_id=share['user_id'],
) )
self.assertTrue(self.share_manager.db.share_get.called) self.assertTrue(self.share_manager.db.share_get.called)
@ -3184,11 +3187,11 @@ class ShareManagerTestCase(test.TestCase):
quota.QUOTAS.reserve.assert_called_once_with( quota.QUOTAS.reserve.assert_called_once_with(
mock.ANY, gigabytes=-size_decrease, project_id=share['project_id'], mock.ANY, gigabytes=-size_decrease, project_id=share['project_id'],
user_id=share['user_id'] share_type_id=None, user_id=share['user_id'],
) )
quota.QUOTAS.commit.assert_called_once_with( quota.QUOTAS.commit.assert_called_once_with(
mock.ANY, mock.ANY, project_id=share['project_id'], mock.ANY, mock.ANY, project_id=share['project_id'],
user_id=share['user_id'] share_type_id=None, user_id=share['user_id'],
) )
manager.db.share_update.assert_called_once_with( manager.db.share_update.assert_called_once_with(
mock.ANY, share_id, shr_update mock.ANY, share_id, shr_update
@ -5508,10 +5511,12 @@ class ShareManagerTestCase(test.TestCase):
share_id = 'fake_share_id' share_id = 'fake_share_id'
share = fakes.fake_share( share = fakes.fake_share(
id=share_id, instance={'id': 'fake_instance_id'}, id=share_id, instance={'id': 'fake_instance_id',
'share_type_id': 'fake_share_type_id'},
project_id='fake_project', user_id='fake_user', size=2) project_id='fake_project', user_id='fake_user', size=2)
snapshot_instance = fakes.fake_snapshot_instance( snapshot_instance = fakes.fake_snapshot_instance(
share_id=share_id, share=share, name='fake_snapshot') share_id=share_id, share=share, name='fake_snapshot',
share_instance=share['instance'])
snapshot = fakes.fake_snapshot( snapshot = fakes.fake_snapshot(
id='fake_snapshot_id', share_id=share_id, share=share, id='fake_snapshot_id', share_id=share_id, share=share,
instance=snapshot_instance, project_id='fake_project', instance=snapshot_instance, project_id='fake_project',
@ -5543,7 +5548,9 @@ class ShareManagerTestCase(test.TestCase):
if reservations: if reservations:
mock_quotas_commit.assert_called_once_with( mock_quotas_commit.assert_called_once_with(
mock.ANY, reservations, project_id='fake_project', mock.ANY, reservations, project_id='fake_project',
user_id='fake_user') user_id='fake_user',
share_type_id=(
snapshot_instance['share_instance']['share_type_id']))
else: else:
self.assertFalse(mock_quotas_commit.called) self.assertFalse(mock_quotas_commit.called)
@ -5567,10 +5574,12 @@ class ShareManagerTestCase(test.TestCase):
share_id = 'fake_share_id' share_id = 'fake_share_id'
share = fakes.fake_share( share = fakes.fake_share(
id=share_id, instance={'id': 'fake_instance_id'}, id=share_id, instance={'id': 'fake_instance_id',
'share_type_id': 'fake_share_type_id'},
project_id='fake_project', user_id='fake_user', size=2) project_id='fake_project', user_id='fake_user', size=2)
snapshot_instance = fakes.fake_snapshot_instance( snapshot_instance = fakes.fake_snapshot_instance(
share_id=share_id, share=share, name='fake_snapshot') share_id=share_id, share=share, name='fake_snapshot',
share_instance=share['instance'])
snapshot = fakes.fake_snapshot( snapshot = fakes.fake_snapshot(
id='fake_snapshot_id', share_id=share_id, share=share, id='fake_snapshot_id', share_id=share_id, share=share,
instance=snapshot_instance, project_id='fake_project', instance=snapshot_instance, project_id='fake_project',
@ -5607,7 +5616,9 @@ class ShareManagerTestCase(test.TestCase):
if reservations: if reservations:
mock_quotas_rollback.assert_called_once_with( mock_quotas_rollback.assert_called_once_with(
mock.ANY, reservations, project_id='fake_project', mock.ANY, reservations, project_id='fake_project',
user_id='fake_user') user_id='fake_user',
share_type_id=(
snapshot_instance['share_instance']['share_type_id']))
else: else:
self.assertFalse(mock_quotas_rollback.called) self.assertFalse(mock_quotas_rollback.called)
@ -5822,7 +5833,7 @@ class ShareManagerTestCase(test.TestCase):
if reservations: if reservations:
mock_quotas_commit.assert_called_once_with( mock_quotas_commit.assert_called_once_with(
mock.ANY, reservations, project_id='fake_project', mock.ANY, reservations, project_id='fake_project',
user_id='fake_user') user_id='fake_user', share_type_id=None)
else: else:
self.assertFalse(mock_quotas_commit.called) self.assertFalse(mock_quotas_commit.called)
@ -5851,10 +5862,12 @@ class ShareManagerTestCase(test.TestCase):
snapshot_instances = [snapshot['instance'], snapshot_instance] snapshot_instances = [snapshot['instance'], snapshot_instance]
active_replica = fake_replica( active_replica = fake_replica(
id='rid1', share_id=share_id, host=self.share_manager.host, id='rid1', share_id=share_id, host=self.share_manager.host,
replica_state=constants.REPLICA_STATE_ACTIVE, as_primitive=False) replica_state=constants.REPLICA_STATE_ACTIVE, as_primitive=False,
share_type_id='fake_share_type_id')
replica = fake_replica( replica = fake_replica(
id='rid2', share_id=share_id, host='secondary', id='rid2', share_id=share_id, host='secondary',
replica_state=constants.REPLICA_STATE_IN_SYNC, as_primitive=False) replica_state=constants.REPLICA_STATE_IN_SYNC, as_primitive=False,
share_type_id='fake_share_type_id')
replicas = [active_replica, replica] replicas = [active_replica, replica]
access_rules = [] access_rules = []
self.mock_object( self.mock_object(
@ -5893,7 +5906,7 @@ class ShareManagerTestCase(test.TestCase):
if reservations: if reservations:
mock_quotas_rollback.assert_called_once_with( mock_quotas_rollback.assert_called_once_with(
mock.ANY, reservations, project_id='fake_project', mock.ANY, reservations, project_id='fake_project',
user_id='fake_user') user_id='fake_user', share_type_id=replica['share_type_id'])
else: else:
self.assertFalse(mock_quotas_rollback.called) self.assertFalse(mock_quotas_rollback.called)

File diff suppressed because it is too large Load Diff

View File

@ -30,7 +30,7 @@ ShareGroup = [
help="The minimum api microversion is configured to be the " help="The minimum api microversion is configured to be the "
"value of the minimum microversion supported by Manila."), "value of the minimum microversion supported by Manila."),
cfg.StrOpt("max_api_microversion", cfg.StrOpt("max_api_microversion",
default="2.38", default="2.39",
help="The maximum api microversion is configured to be the " help="The maximum api microversion is configured to be the "
"value of the latest microversion supported by Manila."), "value of the latest microversion supported by Manila."),
cfg.StrOpt("region", cfg.StrOpt("region",

View File

@ -852,11 +852,23 @@ class SharesV2Client(shares_client.SharesClient):
############### ###############
def _get_quotas_url(self, version): @staticmethod
def _get_quotas_url(version):
if utils.is_microversion_gt(version, "2.6"): if utils.is_microversion_gt(version, "2.6"):
return 'quota-sets' return 'quota-sets'
return 'os-quota-sets' return 'os-quota-sets'
@staticmethod
def _get_quotas_url_arguments_as_str(user_id=None, share_type=None):
args_str = ''
if not (user_id is None or share_type is None):
args_str = "?user_id=%s&share_type=%s" % (user_id, share_type)
elif user_id is not None:
args_str = "?user_id=%s" % user_id
elif share_type is not None:
args_str = "?share_type=%s" % share_type
return args_str
def default_quotas(self, tenant_id, url=None, version=LATEST_MICROVERSION): def default_quotas(self, tenant_id, url=None, version=LATEST_MICROVERSION):
if url is None: if url is None:
url = self._get_quotas_url(version) url = self._get_quotas_url(version)
@ -865,48 +877,44 @@ class SharesV2Client(shares_client.SharesClient):
self.expected_success(200, resp.status) self.expected_success(200, resp.status)
return self._parse_resp(body) return self._parse_resp(body)
def show_quotas(self, tenant_id, user_id=None, url=None, def show_quotas(self, tenant_id, user_id=None, share_type=None, url=None,
version=LATEST_MICROVERSION): version=LATEST_MICROVERSION):
if url is None: if url is None:
url = self._get_quotas_url(version) url = self._get_quotas_url(version)
url += '/%s' % tenant_id url += '/%s' % tenant_id
if user_id is not None: url += self._get_quotas_url_arguments_as_str(user_id, share_type)
url += "?user_id=%s" % user_id
resp, body = self.get(url, version=version) resp, body = self.get(url, version=version)
self.expected_success(200, resp.status) self.expected_success(200, resp.status)
return self._parse_resp(body) return self._parse_resp(body)
def reset_quotas(self, tenant_id, user_id=None, url=None, def reset_quotas(self, tenant_id, user_id=None, share_type=None, url=None,
version=LATEST_MICROVERSION): version=LATEST_MICROVERSION):
if url is None: if url is None:
url = self._get_quotas_url(version) url = self._get_quotas_url(version)
url += '/%s' % tenant_id url += '/%s' % tenant_id
if user_id is not None: url += self._get_quotas_url_arguments_as_str(user_id, share_type)
url += "?user_id=%s" % user_id
resp, body = self.delete(url, version=version) resp, body = self.delete(url, version=version)
self.expected_success(202, resp.status) self.expected_success(202, resp.status)
return body return body
def detail_quotas(self, tenant_id, user_id=None, url=None, def detail_quotas(self, tenant_id, user_id=None, share_type=None, url=None,
version=LATEST_MICROVERSION): version=LATEST_MICROVERSION):
if url is None: if url is None:
url = self._get_quotas_url(version) url = self._get_quotas_url(version)
url += '/%s/detail' % tenant_id url += '/%s/detail' % tenant_id
if user_id is not None: url += self._get_quotas_url_arguments_as_str(user_id, share_type)
url += "?user_id=%s" % user_id
resp, body = self.get(url, version=version) resp, body = self.get(url, version=version)
self.expected_success(200, resp.status) self.expected_success(200, resp.status)
return self._parse_resp(body) return self._parse_resp(body)
def update_quotas(self, tenant_id, user_id=None, shares=None, def update_quotas(self, tenant_id, user_id=None, shares=None,
snapshots=None, gigabytes=None, snapshot_gigabytes=None, snapshots=None, gigabytes=None, snapshot_gigabytes=None,
share_networks=None, force=True, url=None, share_networks=None, force=True, share_type=None,
version=LATEST_MICROVERSION): url=None, version=LATEST_MICROVERSION):
if url is None: if url is None:
url = self._get_quotas_url(version) url = self._get_quotas_url(version)
url += '/%s' % tenant_id url += '/%s' % tenant_id
if user_id is not None: url += self._get_quotas_url_arguments_as_str(user_id, share_type)
url += "?user_id=%s" % user_id
put_body = {"tenant_id": tenant_id} put_body = {"tenant_id": tenant_id}
if force: if force:

View File

@ -13,7 +13,10 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
import ddt
from tempest import config from tempest import config
from tempest.lib.common.utils import data_utils
from tempest.lib import exceptions as lib_exc
from testtools import testcase as tc from testtools import testcase as tc
from manila_tempest_tests.tests.api import base from manila_tempest_tests.tests.api import base
@ -21,6 +24,7 @@ from manila_tempest_tests.tests.api import base
CONF = config.CONF CONF = config.CONF
@ddt.ddt
class SharesAdminQuotasTest(base.BaseSharesAdminTest): class SharesAdminQuotasTest(base.BaseSharesAdminTest):
@classmethod @classmethod
@ -60,7 +64,37 @@ class SharesAdminQuotasTest(base.BaseSharesAdminTest):
self.assertGreater(int(quotas["snapshots"]), -2) self.assertGreater(int(quotas["snapshots"]), -2)
self.assertGreater(int(quotas["share_networks"]), -2) self.assertGreater(int(quotas["share_networks"]), -2)
@ddt.data(
('id', True),
('name', False),
)
@ddt.unpack
@tc.attr(base.TAG_POSITIVE, base.TAG_API)
@base.skip_if_microversion_lt("2.39")
def test_show_share_type_quotas(self, share_type_key, is_st_public):
# Create share type
share_type = self.create_share_type(
data_utils.rand_name("tempest-manila"),
is_public=is_st_public,
cleanup_in_class=False,
extra_specs=self.add_extra_specs_to_dict(),
)
if 'share_type' in share_type:
share_type = share_type['share_type']
# Get current project quotas
p_quotas = self.shares_v2_client.show_quotas(self.tenant_id)
# Get current quotas
st_quotas = self.shares_v2_client.show_quotas(
self.tenant_id, share_type=share_type[share_type_key])
# Share type quotas have values equal to project's
for key in ('shares', 'gigabytes', 'snapshots', 'snapshot_gigabytes'):
self.assertEqual(st_quotas[key], p_quotas[key])
@ddt.ddt
class SharesAdminQuotasUpdateTest(base.BaseSharesAdminTest): class SharesAdminQuotasUpdateTest(base.BaseSharesAdminTest):
force_tenant_isolation = True force_tenant_isolation = True
@ -101,6 +135,47 @@ class SharesAdminQuotasUpdateTest(base.BaseSharesAdminTest):
self.tenant_id, self.user_id, shares=new_quota) self.tenant_id, self.user_id, shares=new_quota)
self.assertEqual(new_quota, int(updated["shares"])) self.assertEqual(new_quota, int(updated["shares"]))
def _create_share_type(self):
share_type = self.create_share_type(
data_utils.rand_name("tempest-manila"),
cleanup_in_class=False,
client=self.shares_v2_client,
extra_specs=self.add_extra_specs_to_dict(),
)
if 'share_type' in share_type:
share_type = share_type['share_type']
return share_type
@ddt.data(
('id', True),
('name', False),
)
@ddt.unpack
@tc.attr(base.TAG_POSITIVE, base.TAG_API)
@base.skip_if_microversion_lt("2.39")
def test_update_share_type_quota(self, share_type_key, is_st_public):
share_type = self._create_share_type()
# Get current quotas
quotas = self.client.show_quotas(
self.tenant_id, share_type=share_type[share_type_key])
# Update quotas
for q in ('shares', 'gigabytes', 'snapshots', 'snapshot_gigabytes'):
new_quota = int(quotas[q]) - 1
# Set new quota
updated = self.client.update_quotas(
self.tenant_id, share_type=share_type[share_type_key],
**{q: new_quota})
self.assertEqual(new_quota, int(updated[q]))
current_quotas = self.client.show_quotas(
self.tenant_id, share_type=share_type[share_type_key])
for q in ('shares', 'gigabytes', 'snapshots', 'snapshot_gigabytes'):
self.assertEqual(int(quotas[q]) - 1, current_quotas[q])
@tc.attr(base.TAG_POSITIVE, base.TAG_API) @tc.attr(base.TAG_POSITIVE, base.TAG_API)
def test_update_tenant_quota_snapshots(self): def test_update_tenant_quota_snapshots(self):
# get current quotas # get current quotas
@ -244,6 +319,51 @@ class SharesAdminQuotasUpdateTest(base.BaseSharesAdminTest):
self.assertEqual(int(default["share_networks"]), self.assertEqual(int(default["share_networks"]),
int(reseted["share_networks"])) int(reseted["share_networks"]))
@ddt.data(
('id', True),
('name', False),
)
@ddt.unpack
@tc.attr(base.TAG_POSITIVE, base.TAG_API)
@base.skip_if_microversion_lt("2.39")
def test_reset_share_type_quotas(self, share_type_key, is_st_public):
share_type = self._create_share_type()
# get default_quotas
default_quotas = self.client.default_quotas(self.tenant_id)
# set new quota for project
updated_p_quota = self.client.update_quotas(
self.tenant_id,
shares=int(default_quotas['shares']) + 5,
snapshots=int(default_quotas['snapshots']) + 5,
gigabytes=int(default_quotas['gigabytes']) + 5,
snapshot_gigabytes=int(default_quotas['snapshot_gigabytes']) + 5)
# set new quota for project
self.client.update_quotas(
self.tenant_id,
share_type=share_type[share_type_key],
shares=int(default_quotas['shares']) + 3,
snapshots=int(default_quotas['snapshots']) + 3,
gigabytes=int(default_quotas['gigabytes']) + 3,
snapshot_gigabytes=int(default_quotas['snapshot_gigabytes']) + 3)
# reset share type quotas
self.client.reset_quotas(
self.tenant_id, share_type=share_type[share_type_key])
# verify quotas
current_p_quota = self.client.show_quotas(self.tenant_id)
current_st_quota = self.client.show_quotas(
self.tenant_id, share_type=share_type[share_type_key])
for key in ('shares', 'snapshots', 'gigabytes', 'snapshot_gigabytes'):
self.assertEqual(updated_p_quota[key], current_p_quota[key])
# Default share type quotas are current project quotas
self.assertNotEqual(default_quotas[key], current_st_quota[key])
self.assertEqual(current_p_quota[key], current_st_quota[key])
@tc.attr(base.TAG_POSITIVE, base.TAG_API) @tc.attr(base.TAG_POSITIVE, base.TAG_API)
def test_unlimited_quota_for_shares(self): def test_unlimited_quota_for_shares(self):
self.client.update_quotas(self.tenant_id, shares=-1) self.client.update_quotas(self.tenant_id, shares=-1)
@ -329,3 +449,95 @@ class SharesAdminQuotasUpdateTest(base.BaseSharesAdminTest):
quotas = self.client.show_quotas(self.tenant_id, self.user_id) quotas = self.client.show_quotas(self.tenant_id, self.user_id)
self.assertEqual(-1, quotas.get('share_networks')) self.assertEqual(-1, quotas.get('share_networks'))
@ddt.data(11, -1)
@tc.attr(base.TAG_NEGATIVE, base.TAG_API)
def test_update_user_quotas_bigger_than_project_quota(self, user_quota):
self.client.update_quotas(self.tenant_id, shares=10)
self.client.update_quotas(
self.tenant_id, user_id=self.user_id, force=True,
shares=user_quota)
@ddt.data(11, -1)
@tc.attr(base.TAG_NEGATIVE, base.TAG_API)
@base.skip_if_microversion_lt("2.39")
def test_update_share_type_quotas_bigger_than_project_quota(self, st_q):
share_type = self._create_share_type()
self.client.update_quotas(self.tenant_id, shares=10)
self.client.update_quotas(
self.tenant_id, share_type=share_type['name'], force=True,
shares=st_q)
@tc.attr(base.TAG_NEGATIVE, base.TAG_API)
@base.skip_if_microversion_lt("2.39")
def test_set_share_type_quota_bigger_than_users_quota(self):
share_type = self._create_share_type()
self.client.update_quotas(self.tenant_id, force=False, shares=13)
self.client.update_quotas(
self.tenant_id, user_id=self.user_id, force=False, shares=11)
# Share type quota does not depend on user's quota, so we should be
# able to update it.
self.client.update_quotas(
self.tenant_id, share_type=share_type['name'], force=False,
shares=12)
@tc.attr(base.TAG_POSITIVE, base.TAG_API_WITH_BACKEND)
@base.skip_if_microversion_lt("2.39")
def test_quotas_usages(self):
# Create share types
st_1, st_2 = (self._create_share_type() for i in (1, 2))
# Set quotas for project, user and both share types
self.client.update_quotas(self.tenant_id, shares=3, gigabytes=10)
self.client.update_quotas(
self.tenant_id, user_id=self.user_id, shares=2, gigabytes=7)
for st in (st_1['id'], st_2['name']):
self.client.update_quotas(
self.tenant_id, share_type=st, shares=2, gigabytes=4)
# Create share, 4Gb, st1 - ok
share_1 = self.create_share(
size=4, share_type_id=st_1['id'], client=self.client,
cleanup_in_class=False)
# Try create shares twice, failing on user and share type quotas
for size, st_id in ((3, st_1['id']), (4, st_2['id'])):
self.assertRaises(
lib_exc.OverLimit,
self.create_share,
size=size, share_type_id=st_id, client=self.client,
cleanup_in_class=False)
# Create share, 3Gb, st2 - ok
share_2 = self.create_share(
size=3, share_type_id=st_2['id'], client=self.client,
cleanup_in_class=False)
# Check quota usages
for g_l, g_use, s_l, s_use, kwargs in (
(10, 7, 3, 2, {}),
(7, 7, 2, 2, {'user_id': self.user_id}),
(4, 4, 2, 1, {'share_type': st_1['id']}),
(4, 3, 2, 1, {'share_type': st_2['name']})):
quotas = self.client.detail_quotas(
tenant_id=self.tenant_id, **kwargs)
self.assertEqual(0, quotas['gigabytes']['reserved'])
self.assertEqual(g_l, quotas['gigabytes']['limit'])
self.assertEqual(g_use, quotas['gigabytes']['in_use'])
self.assertEqual(0, quotas['shares']['reserved'])
self.assertEqual(s_l, quotas['shares']['limit'])
self.assertEqual(s_use, quotas['shares']['in_use'])
# Delete shares and then check usages
for share_id in (share_1['id'], share_2['id']):
self.client.delete_share(share_id)
self.client.wait_for_resource_deletion(share_id=share_id)
for kwargs in ({}, {'share_type': st_1['name']},
{'user_id': self.user_id}, {'share_type': st_2['id']}):
quotas = self.client.detail_quotas(
tenant_id=self.tenant_id, **kwargs)
for key in ('shares', 'gigabytes'):
self.assertEqual(0, quotas[key]['reserved'])
self.assertEqual(0, quotas[key]['in_use'])

View File

@ -15,6 +15,7 @@
import ddt import ddt
from tempest import config from tempest import config
from tempest.lib.common.utils import data_utils
from tempest.lib import exceptions as lib_exc from tempest.lib import exceptions as lib_exc
from testtools import testcase as tc from testtools import testcase as tc
@ -117,6 +118,7 @@ class SharesAdminQuotasNegativeTest(base.BaseSharesAdminTest):
client.update_quotas, client.update_quotas,
client.tenant_id, client.tenant_id,
client.user_id, client.user_id,
force=False,
shares=bigger_value) shares=bigger_value)
@tc.attr(base.TAG_NEGATIVE, base.TAG_API) @tc.attr(base.TAG_NEGATIVE, base.TAG_API)
@ -132,6 +134,7 @@ class SharesAdminQuotasNegativeTest(base.BaseSharesAdminTest):
client.update_quotas, client.update_quotas,
client.tenant_id, client.tenant_id,
client.user_id, client.user_id,
force=False,
snapshots=bigger_value) snapshots=bigger_value)
@tc.attr(base.TAG_NEGATIVE, base.TAG_API) @tc.attr(base.TAG_NEGATIVE, base.TAG_API)
@ -147,6 +150,7 @@ class SharesAdminQuotasNegativeTest(base.BaseSharesAdminTest):
client.update_quotas, client.update_quotas,
client.tenant_id, client.tenant_id,
client.user_id, client.user_id,
force=False,
gigabytes=bigger_value) gigabytes=bigger_value)
@tc.attr(base.TAG_NEGATIVE, base.TAG_API) @tc.attr(base.TAG_NEGATIVE, base.TAG_API)
@ -162,6 +166,7 @@ class SharesAdminQuotasNegativeTest(base.BaseSharesAdminTest):
client.update_quotas, client.update_quotas,
client.tenant_id, client.tenant_id,
client.user_id, client.user_id,
force=False,
snapshot_gigabytes=bigger_value) snapshot_gigabytes=bigger_value)
@tc.attr(base.TAG_NEGATIVE, base.TAG_API) @tc.attr(base.TAG_NEGATIVE, base.TAG_API)
@ -177,6 +182,7 @@ class SharesAdminQuotasNegativeTest(base.BaseSharesAdminTest):
client.update_quotas, client.update_quotas,
client.tenant_id, client.tenant_id,
client.user_id, client.user_id,
force=False,
share_networks=bigger_value) share_networks=bigger_value)
@ddt.data( @ddt.data(
@ -215,3 +221,98 @@ class SharesAdminQuotasNegativeTest(base.BaseSharesAdminTest):
self.shares_v2_client.tenant_id, self.shares_v2_client.tenant_id,
version=version, url=url, version=version, url=url,
) )
@ddt.data('show', 'reset', 'update')
@tc.attr(base.TAG_NEGATIVE, base.TAG_API)
@base.skip_if_microversion_lt("2.39")
def test_share_type_quotas_using_nonexistent_share_type(self, op):
client = self.get_client_with_isolated_creds(client_version='2')
kwargs = {"share_type": "fake_nonexistent_share_type"}
if op == 'update':
tenant_quotas = client.show_quotas(client.tenant_id)
kwargs['shares'] = tenant_quotas['shares']
self.assertRaises(
lib_exc.NotFound,
getattr(client, op + '_quotas'),
client.tenant_id,
**kwargs)
def _create_share_type(self):
share_type = self.create_share_type(
data_utils.rand_name("tempest-manila"),
cleanup_in_class=False,
client=self.shares_v2_client,
extra_specs=self.add_extra_specs_to_dict(),
)
if 'share_type' in share_type:
share_type = share_type['share_type']
return share_type
@ddt.data('id', 'name')
@tc.attr(base.TAG_NEGATIVE, base.TAG_API)
@base.skip_if_microversion_lt("2.39")
def test_try_update_share_type_quota_for_share_networks(self, key):
client = self.get_client_with_isolated_creds(client_version='2')
share_type = self._create_share_type()
tenant_quotas = client.show_quotas(client.tenant_id)
# Try to set 'share_networks' quota for share type
self.assertRaises(
lib_exc.BadRequest,
client.update_quotas,
client.tenant_id,
share_type=share_type[key],
share_networks=int(tenant_quotas["share_networks"]),
)
@ddt.data('show', 'reset', 'update')
@tc.attr(base.TAG_NEGATIVE, base.TAG_API)
@base.skip_if_microversion_lt("2.38")
def test_share_type_quotas_using_too_old_microversion(self, op):
client = self.get_client_with_isolated_creds(client_version='2')
share_type = self._create_share_type()
kwargs = {"version": "2.38", "share_type": share_type["name"]}
if op == 'update':
tenant_quotas = client.show_quotas(client.tenant_id)
kwargs['shares'] = tenant_quotas['shares']
self.assertRaises(
lib_exc.BadRequest,
getattr(client, op + '_quotas'),
client.tenant_id,
**kwargs)
@ddt.data('show', 'reset', 'update')
@tc.attr(base.TAG_NEGATIVE, base.TAG_API)
@base.skip_if_microversion_lt("2.39")
def test_quotas_providing_share_type_and_user_id(self, op):
client = self.get_client_with_isolated_creds(client_version='2')
share_type = self._create_share_type()
kwargs = {"share_type": share_type["name"], "user_id": client.user_id}
if op == 'update':
tenant_quotas = client.show_quotas(client.tenant_id)
kwargs['shares'] = tenant_quotas['shares']
self.assertRaises(
lib_exc.BadRequest,
getattr(client, op + '_quotas'),
client.tenant_id,
**kwargs)
@ddt.data(11, -1)
@tc.attr(base.TAG_NEGATIVE, base.TAG_API)
@base.skip_if_microversion_lt("2.39")
def test_update_share_type_quotas_bigger_than_project_quota(self, st_q):
client = self.get_client_with_isolated_creds(client_version='2')
share_type = self._create_share_type()
client.update_quotas(client.tenant_id, shares=10)
self.assertRaises(
lib_exc.BadRequest,
client.update_quotas,
client.tenant_id,
share_type=share_type['name'],
force=False,
shares=st_q)

View File

@ -0,0 +1,5 @@
---
features:
- Added possibility to set quotas per share type.
It is useful for deployments with multiple backends that
are accessible via different share types.