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.38 - Support IPv6 validation in allow_access API to enable IPv6 in
manila.
* 2.39 - Added share-type quotas.
"""
# The minimum and maximum versions of the API supported
# The default api version request is defined to be the
# minimum version of the API supported.
_MIN_API_VERSION = "2.0"
_MAX_API_VERSION = "2.38"
_MAX_API_VERSION = "2.39"
DEFAULT_API_VERSION = _MIN_API_VERSION

View File

@ -218,3 +218,7 @@ user documentation.
2.38
----
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
import webob
from manila.api.openstack import api_version_request as api_version
from manila.api.openstack import wsgi
from manila.api.views import quota_sets as quota_sets_views
from manila import db
@ -28,7 +29,7 @@ from manila import quota
QUOTAS = quota.QUOTAS
LOG = log.getLogger(__name__)
NON_QUOTA_KEYS = ('tenant_id', 'id', 'force')
NON_QUOTA_KEYS = ('tenant_id', 'id', 'force', 'share_type')
class QuotaSetsMixin(object):
@ -41,7 +42,8 @@ class QuotaSetsMixin(object):
resource_name = "quota_set"
_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
if limit < -1:
msg = _("Quota limit must be -1 or greater.")
@ -50,17 +52,49 @@ class QuotaSetsMixin(object):
(maximum != -1 or (maximum == -1 and limit != -1))):
msg = _("Quota limit must be greater than %s.") % minimum
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
raise webob.exc.HTTPBadRequest(explanation=msg)
def _get_quotas(self, context, id, user_id=None, usages=False):
if user_id:
values = QUOTAS.get_user_quotas(context, id, user_id,
usages=usages)
else:
values = QUOTAS.get_project_quotas(context, id, usages=usages)
@staticmethod
def _validate_user_id_and_share_type_args(user_id, share_type):
if user_id and share_type:
msg = _("'user_id' and 'share_type' values are mutually exclusive")
raise webob.exc.HTTPBadRequest(explanation=msg)
@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:
return values
return {k: v['limit'] for k, v in values.items()}
@ -70,14 +104,15 @@ class QuotaSetsMixin(object):
context = req.environ['manila.context']
params = parse.parse_qs(req.environ.get('QUERY_STRING', ''))
user_id = params.get('user_id', [None])[0]
share_type = params.get('share_type', [None])[0]
try:
db.authorize_project_context(context, id)
# _get_quotas use 'usages' to indicate whether retrieve additional
# attributes, so pass detail to the argument.
return self._view_builder.detail_list(
self._get_quotas(context, id, user_id=user_id,
usages=detail), id)
share_type_id = self._get_share_type_id(context, share_type)
quotas = self._get_quotas(
context, id, user_id, share_type_id, usages=detail)
return self._view_builder.detail_list(quotas, id, share_type_id)
except exception.NotAuthorized:
raise webob.exc.HTTPForbidden()
@ -94,19 +129,25 @@ class QuotaSetsMixin(object):
force_update = False
params = parse.parse_qs(req.environ.get('QUERY_STRING', ''))
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:
settable_quotas = QUOTAS.get_settable_quotas(context, project_id,
user_id=user_id)
settable_quotas = QUOTAS.get_settable_quotas(
context, project_id, user_id=user_id,
share_type_id=share_type_id)
except exception.NotAuthorized:
raise webob.exc.HTTPForbidden()
for key, value in body.get('quota_set', {}).items():
if (key not in QUOTAS and
key not in NON_QUOTA_KEYS):
if key == 'share_networks' and share_type_id:
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)
continue
if key == 'force':
elif key == 'force':
force_update = strutils.bool_from_string(value)
elif key not in NON_QUOTA_KEYS and value:
try:
@ -124,8 +165,9 @@ class QuotaSetsMixin(object):
raise webob.exc.HTTPBadRequest(explanation=msg)
try:
quotas = self._get_quotas(context, id, user_id=user_id,
usages=True)
quotas = self._get_quotas(
context, id, user_id=user_id, share_type_id=share_type_id,
usages=True)
except exception.NotAuthorized:
raise webob.exc.HTTPForbidden()
@ -164,25 +206,36 @@ class QuotaSetsMixin(object):
maximum = settable_quotas[key]['maximum']
self._validate_quota_limit(value, minimum, maximum, force_update)
try:
db.quota_create(context, project_id, key, value,
user_id=user_id)
db.quota_create(
context, project_id, key, value,
user_id=user_id, share_type_id=share_type_id)
except exception.QuotaExists:
db.quota_update(context, project_id, key, value,
user_id=user_id)
db.quota_update(
context, project_id, key, value,
user_id=user_id, share_type_id=share_type_id)
except exception.AdminRequired:
raise webob.exc.HTTPForbidden()
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")
def _delete(self, req, id):
context = req.environ['manila.context']
params = parse.parse_qs(req.environ.get('QUERY_STRING', ''))
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:
db.authorize_project_context(context, id)
if 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:
QUOTAS.destroy_all_by_project(context, id)
return webob.Response(status_int=202)
@ -199,6 +252,7 @@ class QuotaSetsControllerLegacy(QuotaSetsMixin, wsgi.Controller):
@wsgi.Controller.api_version('1.0', '2.6')
def show(self, req, id):
self._ensure_share_type_arg_is_absent(req)
return self._show(req, id)
@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')
def update(self, req, id, body):
self._ensure_share_type_arg_is_absent(req)
return self._update(req, id, body)
@wsgi.Controller.api_version('1.0', '2.6')
def delete(self, req, id):
self._ensure_share_type_arg_is_absent(req)
return self._delete(req, id)
@ -223,10 +279,14 @@ class QuotaSetsController(QuotaSetsMixin, wsgi.Controller):
@wsgi.Controller.api_version('2.7')
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)
@wsgi.Controller.api_version('2.25')
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)
@wsgi.Controller.api_version('2.7')
@ -235,10 +295,14 @@ class QuotaSetsController(QuotaSetsMixin, wsgi.Controller):
@wsgi.Controller.api_version('2.7')
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)
@wsgi.Controller.api_version('2.7')
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)

View File

@ -20,16 +20,17 @@ class ViewBuilder(common.ViewBuilder):
_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."""
keys = (
'shares',
'gigabytes',
'snapshots',
'snapshot_gigabytes',
'share_networks',
)
view = {key: quota_set.get(key) for key in keys}
if project_id:
view['id'] = project_id
if not share_type:
view['share_networks'] = quota_set.get('share_networks')
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."""
return IMPL.quota_create(context, project_id, resource, limit,
user_id=user_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)
user_id=user_id, share_type_id=share_type_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)
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):
"""Retrieve all quotas associated with a given project."""
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)
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."""
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."""
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):
@ -210,47 +217,61 @@ def quota_usage_get_all_by_project_and_user(context, 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):
"""Retrieve all usage associated with a given resource."""
return IMPL.quota_usage_get_all_by_project(context, project_id)
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."""
return IMPL.quota_usage_create(context, project_id, user_id, resource,
in_use, reserved, until_refresh)
return IMPL.quota_usage_create(
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."""
return IMPL.quota_usage_update(context, project_id, user_id, resource,
**kwargs)
return IMPL.quota_usage_update(
context, project_id, user_id, resource, share_type_id=share_type_id,
**kwargs)
###################
def quota_reserve(context, resources, quotas, user_quotas, deltas, expire,
until_refresh, max_age, project_id=None, user_id=None):
def quota_reserve(context, resources, quotas, user_quotas, share_type_quotas,
deltas, expire, until_refresh, max_age,
project_id=None, user_id=None, share_type_id=None):
"""Check quotas and create appropriate reservations."""
return IMPL.quota_reserve(context, resources, quotas, user_quotas, deltas,
expire, until_refresh, max_age,
project_id=project_id, user_id=user_id)
return IMPL.quota_reserve(
context, resources, quotas, user_quotas, share_type_quotas, deltas,
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."""
return IMPL.reservation_commit(context, reservations,
project_id=project_id,
user_id=user_id)
return IMPL.reservation_commit(
context, reservations, project_id=project_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."""
return IMPL.reservation_rollback(context, reservations,
project_id=project_id,
user_id=user_id)
return IMPL.reservation_rollback(
context, reservations, project_id=project_id, user_id=user_id,
share_type_id=share_type_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)
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):
"""Destroy all quotas associated with a given project."""
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)
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'
from alembic import op
from sqlalchemy import Index, MetaData, Table
def _reservation_index(method):
meta = MetaData()
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)
INDEX_NAME = 'reservations_deleted_expire_idx'
TABLE_NAME = 'reservations'
def upgrade():
_reservation_index('create')
op.create_index(INDEX_NAME, TABLE_NAME, ['deleted', 'expire'])
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 utils as db_utils
from oslo_log import log
from oslo_utils import excutils
from oslo_utils import timeutils
from oslo_utils import uuidutils
import six
@ -279,40 +280,41 @@ def ensure_model_dict_has_id(model_dict):
return model_dict
def _sync_shares(context, project_id, user_id, session):
(shares, gigs) = share_data_get_for_project(context,
project_id,
user_id,
session=session)
def _sync_shares(context, project_id, user_id, session, share_type_id=None):
(shares, gigs) = share_data_get_for_project(
context, project_id, user_id, share_type_id=share_type_id,
session=session)
return {'shares': shares}
def _sync_snapshots(context, project_id, user_id, session):
(snapshots, gigs) = snapshot_data_get_for_project(context,
project_id,
user_id,
session=session)
def _sync_snapshots(context, project_id, user_id, session, share_type_id=None):
(snapshots, gigs) = snapshot_data_get_for_project(
context, project_id, user_id, share_type_id=share_type_id,
session=session)
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(
context, project_id, user_id, session=session)
return dict(gigabytes=share_gigs)
context, project_id, user_id, share_type_id=share_type_id,
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(
context, project_id, user_id, session=session)
return dict(snapshot_gigabytes=snapshot_gigs)
context, project_id, user_id, share_type_id=share_type_id,
session=session)
return {"snapshot_gigabytes": snapshot_gigs}
def _sync_share_networks(context, project_id, user_id, session):
share_networks = share_network_get_all_by_project(context,
project_id,
user_id,
session=session)
return {'share_networks': len(share_networks)}
def _sync_share_networks(context, project_id, user_id, session,
share_type_id=None):
share_networks_count = count_share_networks(
context, project_id, user_id, share_type_id=share_type_id,
session=session)
return {'share_networks': share_networks_count}
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
def quota_get_all_by_project_and_user(context, project_id, user_id):
authorize_project_context(context, project_id)
user_quotas = (model_query(context, models.ProjectUserQuota,
models.ProjectUserQuota.resource,
models.ProjectUserQuota.hard_limit).
filter_by(project_id=project_id).
filter_by(user_id=user_id).
all())
user_quotas = model_query(
context, models.ProjectUserQuota,
models.ProjectUserQuota.resource,
models.ProjectUserQuota.hard_limit,
).filter_by(
project_id=project_id,
).filter_by(
user_id=user_id,
).all()
result = {'project_id': project_id, 'user_id': user_id}
for quota in user_quotas:
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
@require_context
def quota_get_all_by_project(context, project_id):
authorize_project_context(context, project_id)
rows = (model_query(context, models.Quota, read_deleted="no").
filter_by(project_id=project_id).
all())
project_quotas = model_query(
context, models.Quota, read_deleted="no",
).filter_by(
project_id=project_id,
).all()
result = {'project_id': project_id}
for row in rows:
result[row.resource] = row.hard_limit
for quota in project_quotas:
result[quota.resource] = quota.hard_limit
return result
@ -518,26 +530,35 @@ def quota_get_all(context, project_id):
@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
if per_user:
check = (model_query(context, models.ProjectUserQuota).
filter_by(project_id=project_id).
filter_by(user_id=user_id).
filter_by(resource=resource).
all())
check = model_query(context, models.ProjectUserQuota).filter(
models.ProjectUserQuota.project_id == project_id,
models.ProjectUserQuota.user_id == user_id,
models.ProjectUserQuota.resource == resource,
).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:
check = (model_query(context, models.Quota).
filter_by(project_id=project_id).
filter_by(resource=resource).
all())
check = model_query(context, models.Quota).filter(
models.Quota.project_id == project_id,
models.Quota.resource == resource,
).all()
quota_ref = models.Quota()
if check:
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.resource = resource
quota_ref.hard_limit = limit
@ -549,22 +570,36 @@ def quota_create(context, project_id, resource, limit, user_id=None):
@require_admin_context
@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
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:
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})
if not result:
if per_user:
raise exception.ProjectUserQuotaNotFound(project_id=project_id,
user_id=user_id)
else:
raise exception.ProjectQuotaNotFound(project_id=project_id)
raise exception.ProjectUserQuotaNotFound(
project_id=project_id, user_id=user_id)
elif share_type_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
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").
filter_by(project_id=project_id).
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()
else:
result = query.filter_by(user_id=None).first()
elif share_type_id:
result = query.filter_by(queryshare_type_id=share_type_id).first()
else:
result = query.first()
@ -658,7 +696,8 @@ def quota_usage_get(context, project_id, resource, user_id=None):
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)
query = (model_query(context, models.QuotaUsage, read_deleted="no").
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,
models.QuotaUsage.user_id is None))
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()
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)
@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,
reserved, until_refresh, session=None):
reserved, until_refresh, share_type_id=None,
session=None):
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.user_id = user_id
quota_usage_ref.resource = resource
quota_usage_ref.in_use = in_use
quota_usage_ref.reserved = reserved
@ -709,27 +764,31 @@ def _quota_usage_create(context, project_id, user_id, resource, in_use,
@require_admin_context
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()
return _quota_usage_create(context, project_id, user_id, resource, in_use,
reserved, until_refresh, session)
return _quota_usage_create(
context, project_id, user_id, resource, in_use, reserved,
until_refresh, share_type_id=share_type_id, session=session)
@require_admin_context
@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 = {}
for key in ['in_use', 'reserved', 'until_refresh']:
for key in ('in_use', 'reserved', 'until_refresh'):
if key in kwargs:
updates[key] = kwargs[key]
result = (model_query(context, models.QuotaUsage, read_deleted="no").
filter_by(project_id=project_id).
filter_by(resource=resource).
filter(or_(models.QuotaUsage.user_id == user_id,
models.QuotaUsage.user_id is None)).
update(updates))
query = model_query(
context, models.QuotaUsage, read_deleted="no",
).filter_by(project_id=project_id).filter_by(resource=resource)
if share_type_id:
query = query.filter_by(share_type_id=share_type_id)
else:
query = query.filter(or_(models.QuotaUsage.user_id == user_id,
models.QuotaUsage.user_id is None))
result = query.update(updates)
if not result:
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,
delta, expire, session=None):
delta, expire, share_type_id=None, session=None):
reservation_ref = models.Reservation()
reservation_ref.uuid = uuid
reservation_ref.usage_id = usage['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.delta = delta
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
# 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):
# Broken out for testability
rows = (model_query(context, models.QuotaUsage,
@ -778,6 +850,7 @@ def _get_project_quota_usages(context, session, project_id):
read_deleted="no",
session=session).
filter_by(project_id=project_id).
filter(models.QuotaUsage.share_type_id is None).
with_lockmode('update').
all())
result = dict()
@ -795,24 +868,49 @@ def _get_project_quota_usages(context, session, project_id):
@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)
def quota_reserve(context, resources, project_quotas, user_quotas, deltas,
expire, until_refresh, max_age, project_id=None,
user_id=None):
def _quota_reserve(context, resources, project_quotas, user_or_st_quotas,
deltas, expire, until_refresh,
max_age, project_id=None, user_id=None, share_type_id=None):
elevated = context.elevated()
session = get_session()
with session.begin():
if project_id is None:
project_id = context.project_id
if user_id is None:
user_id = context.user_id
if share_type_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
user_usages = _get_user_quota_usages(context, session,
project_id, user_id)
project_usages = _get_project_quota_usages(context, session,
project_id)
project_usages = _get_project_quota_usages(
context, session, project_id)
# Handle usage refresh
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?
refresh = False
if ((resource not in PER_PROJECT_QUOTAS) and
(resource not in user_usages)):
user_usages[resource] = _quota_usage_create(
(resource not in user_or_st_usages)):
user_or_st_usages[resource] = _quota_usage_create(
elevated,
project_id,
user_id,
resource,
0, 0,
until_refresh or None,
share_type_id=share_type_id,
session=session)
refresh = True
elif ((resource in PER_PROJECT_QUOTAS) and
(resource not in user_usages)):
user_usages[resource] = _quota_usage_create(
(resource not in user_or_st_usages)):
user_or_st_usages[resource] = _quota_usage_create(
elevated,
project_id,
None,
resource,
0, 0,
until_refresh or None,
share_type_id=share_type_id,
session=session)
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
# heal from that...
refresh = True
elif user_usages[resource].until_refresh is not None:
user_usages[resource].until_refresh -= 1
if user_usages[resource].until_refresh <= 0:
elif user_or_st_usages[resource].until_refresh is not None:
user_or_st_usages[resource].until_refresh -= 1
if user_or_st_usages[resource].until_refresh <= 0:
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:
refresh = True
@ -860,46 +960,54 @@ def quota_reserve(context, resources, project_quotas, user_quotas, deltas,
# Grab the sync routine
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():
# Make sure we have a destination for the usage!
if ((res not in PER_PROJECT_QUOTAS) and
(res not in user_usages)):
user_usages[res] = _quota_usage_create(
(res not in user_or_st_usages)):
user_or_st_usages[res] = _quota_usage_create(
elevated,
project_id,
user_id,
res,
0, 0,
until_refresh or None,
share_type_id=share_type_id,
session=session)
if ((res in PER_PROJECT_QUOTAS) and
(res not in user_usages)):
user_usages[res] = _quota_usage_create(
(res not in user_or_st_usages)):
user_or_st_usages[res] = _quota_usage_create(
elevated,
project_id,
None,
res,
0, 0,
until_refresh or None,
share_type_id=share_type_id,
session=session)
if user_usages[res].in_use != in_use:
LOG.debug('quota_usages out of sync, updating. '
'project_id: %(project_id)s, '
'user_id: %(user_id)s, '
'resource: %(res)s, '
'tracked usage: %(tracked_use)s, '
'actual usage: %(in_use)s',
{'project_id': project_id,
'user_id': user_id,
'res': res,
'tracked_use': user_usages[res].in_use,
'in_use': in_use})
if user_or_st_usages[res].in_use != in_use:
LOG.debug(
'quota_usages out of sync, updating. '
'project_id: %(project_id)s, '
'user_id: %(user_id)s, '
'share_type_id: %(share_type_id)s, '
'resource: %(res)s, '
'tracked usage: %(tracked_use)s, '
'actual usage: %(in_use)s',
{'project_id': project_id,
'user_id': user_id,
'share_type_id': share_type_id,
'res': res,
'tracked_use': user_or_st_usages[res].in_use,
'in_use': in_use})
# Update the usage
user_usages[res].in_use = in_use
user_usages[res].until_refresh = until_refresh or None
user_or_st_usages[res].in_use = in_use
user_or_st_usages[res].until_refresh = (
until_refresh or None)
# Because more than one resource may be refreshed
# 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
unders = [res for res, delta in deltas.items()
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
# NOTE(Vek): We're only concerned about positive increments.
# If a project has gone over quota, we want them to
# be able to reduce their usage without any
# problems.
for key, value in user_usages.items():
for key, value in user_or_st_usages.items():
if key not in project_usages:
project_usages[key] = value
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_usages[res]['total'] or
user_quotas[res] < delta +
user_usages[res].total)]
user_or_st_quotas[res] < delta +
user_or_st_usages[res].total)]
# NOTE(Vek): The quota check needs to be in the transaction,
# 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():
reservation = _reservation_create(elevated,
uuidutils.generate_uuid(),
user_usages[res],
user_or_st_usages[res],
project_id,
user_id,
res, delta, expire,
share_type_id=share_type_id,
session=session)
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
# reserved value if the delta is positive.
if delta > 0:
user_usages[res].reserved += delta
user_or_st_usages[res].reserved += delta
# 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)
if unders:
LOG.warning("Change will make usage less than 0 for the following "
"resources: %s", unders)
if overs:
if project_quotas == user_quotas:
if project_quotas == user_or_st_quotas:
usages = project_usages
else:
usages = user_usages
usages = user_or_st_usages
usages = {k: dict(in_use=v['in_use'], reserved=v['reserved'])
for k, v in usages.items()}
raise exception.OverQuota(overs=sorted(overs), quotas=user_quotas,
usages=usages)
raise exception.OverQuota(
overs=sorted(overs), quotas=user_or_st_quotas, usages=usages)
return reservations
@ -1001,13 +1110,25 @@ def _quota_reservations_query(session, context, reservations):
@require_context
@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()
with session.begin():
usages = _get_user_quota_usages(context, session, project_id, user_id)
reservation_query = _quota_reservations_query(session, context,
reservations)
if share_type_id:
st_usages = _get_share_type_quota_usages(
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():
if reservation['share_type_id']:
usages = st_usages
else:
usages = user_usages
usage = usages[reservation.resource]
if reservation.delta >= 0:
usage.reserved -= reservation.delta
@ -1017,13 +1138,25 @@ def reservation_commit(context, reservations, project_id=None, user_id=None):
@require_context
@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()
with session.begin():
usages = _get_user_quota_usages(context, session, project_id, user_id)
reservation_query = _quota_reservations_query(session, context,
reservations)
if share_type_id:
st_usages = _get_share_type_quota_usages(
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():
if reservation['share_type_id']:
usages = st_usages
else:
usages = user_usages
usage = usages[reservation.resource]
if reservation.delta >= 0:
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))
@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
def quota_destroy_all_by_project(context, project_id):
session = get_session()
@ -1524,18 +1688,19 @@ def share_create(context, share_values, create_share_instance=True):
@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,
func.count(models.Share.id),
func.sum(models.Share.size),
read_deleted="no",
session=session).
filter_by(project_id=project_id))
if user_id:
result = query.filter_by(user_id=user_id).first()
else:
result = query.first()
if share_type_id:
query = query.join("instances").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)
@ -2161,7 +2326,8 @@ def share_snapshot_create(context, create_values,
@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,
func.count(models.ShareSnapshot.id),
func.sum(models.ShareSnapshot.size),
@ -2169,10 +2335,14 @@ def snapshot_data_get_for_project(context, project_id, user_id, session=None):
session=session).
filter_by(project_id=project_id))
if user_id:
result = query.filter_by(user_id=user_id).first()
else:
result = query.first()
if share_type_id:
query = query.join(
models.ShareInstance,
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)
@ -3046,13 +3216,8 @@ def share_network_get_all(context):
@require_context
def share_network_get_all_by_project(context, project_id, user_id=None,
session=None):
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()
def share_network_get_all_by_project(context, project_id):
return _network_get_query(context).filter_by(project_id=project_id).all()
@require_context
@ -3123,6 +3288,22 @@ def share_network_remove_security_service(context, id, security_service_id):
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)
resource = Column(String(255))
hard_limit = Column(Integer, nullable=True)
@ -121,6 +122,19 @@ class ProjectUserQuota(BASE, ManilaBase):
user_id = 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)
@ -149,6 +163,7 @@ class QuotaUsage(BASE, ManilaBase):
project_id = Column(String(255), index=True)
user_id = Column(String(255))
share_type_id = Column(String(36))
resource = Column(String(255))
in_use = Column(Integer)
@ -172,6 +187,7 @@ class Reservation(BASE, ManilaBase):
project_id = Column(String(255), index=True)
user_id = Column(String(255))
share_type_id = Column(String(36))
resource = Column(String(255))
delta = Column(Integer)

View File

@ -330,6 +330,11 @@ class ProjectUserQuotaNotFound(QuotaNotFound):
"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):
message = _("Quota for project %(project_id)s could not be found.")
@ -395,19 +400,27 @@ class QuotaError(ManilaException):
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):
message = _("Requested snapshot exceeds allowed gigabytes quota.")
message = _(
"Requested snapshot exceeds allowed project/user or share type "
"gigabytes quota.")
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):
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):

View File

@ -69,15 +69,6 @@ class DbQuotaDriver(object):
quota information. The default driver utilizes the local
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):
"""Get a specific quota by quota class."""
@ -96,7 +87,6 @@ class DbQuotaDriver(object):
for resource in resources.values():
quotas[resource.name] = default_quotas.get(resource.name,
resource.default)
return quotas
def get_class_quotas(self, context, resources, quota_class,
@ -209,8 +199,7 @@ class DbQuotaDriver(object):
remains=remains)
def get_user_quotas(self, context, resources, project_id, user_id,
quota_class=None, defaults=True,
usages=True):
quota_class=None, defaults=True, usages=True):
"""Retrieve quotas for user and project.
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
will also be returned.
"""
user_quotas = db.quota_get_all_by_project_and_user(context,
project_id, user_id)
user_quotas = db.quota_get_all_by_project_and_user(
context, project_id, user_id)
# Use the project quota for default user quota.
proj_quotas = db.quota_get_all_by_project(context, project_id)
for key, value in proj_quotas.items():
@ -247,8 +236,47 @@ class DbQuotaDriver(object):
user_quotas, quota_class,
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,
user_id=None):
user_id=None, share_type_id=None):
"""Retrieve range of settable quotas.
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 project_id: The ID of the project 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 = {}
project_quotas = self.get_project_quotas(context, resources,
project_id, remains=True)
if user_id:
user_quotas = self.get_user_quotas(context, resources,
project_id, user_id)
setted_quotas = db.quota_get_all_by_project_and_user(
context, project_id, user_id)
for key, value in user_quotas.items():
maximum = (project_quotas[key]['remains'] +
setted_quotas.get(key, 0))
settable_quotas[key] = dict(
minimum=value['in_use'] + value['reserved'],
maximum=maximum)
project_quotas = self.get_project_quotas(
context, resources, project_id, remains=True)
if user_id or share_type_id:
if user_id:
subquotas = self.get_user_quotas(
context, resources, project_id, user_id)
else:
subquotas = self.get_share_type_quotas(
context, resources, project_id, share_type_id)
for key, value in subquotas.items():
settable_quotas[key] = {
"minimum": value['in_use'] + value['reserved'],
"maximum": project_quotas[key]["limit"],
}
else:
for key, value in project_quotas.items():
minimum = max(int(value['limit'] - value['remains']),
int(value['in_use'] + value['reserved']))
settable_quotas[key] = dict(minimum=minimum, maximum=-1)
minimum = max(
int(value['limit'] - value['remains']),
int(value['in_use'] + value['reserved'])
)
settable_quotas[key] = {"minimum": minimum, "maximum": -1}
return settable_quotas
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.
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,
project_id, user_id,
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:
# Grab and return the quotas (without usages)
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()}
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,
project_id=None, user_id=None):
project_id=None, user_id=None, share_type_id=None):
"""Check quotas and reserve resources.
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.
# Yes, the admin may be in the process of reducing
# quotas, but that's a pretty rare thing.
quotas = self._get_quotas(context, resources, deltas.keys(),
has_sync=True, project_id=project_id)
user_quotas = self._get_quotas(context, resources, deltas.keys(),
has_sync=True, project_id=project_id,
user_id=user_id)
quotas = self._get_quotas(
context, resources, deltas, has_sync=True, project_id=project_id)
user_quotas = self._get_quotas(
context, resources, deltas, has_sync=True,
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
# API, because we have to do it in a transaction,
# which means access to the session. Since the
# session isn't available outside the DBAPI, we
# have to do the work there.
return db.quota_reserve(context, resources, quotas, user_quotas,
deltas, expire,
CONF.until_refresh, CONF.max_age,
project_id=project_id, user_id=user_id)
return db.quota_reserve(
context, resources, quotas, user_quotas, share_type_quotas,
deltas, expire, CONF.until_refresh, CONF.max_age,
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.
:param context: The request context, for access checks.
@ -488,10 +475,12 @@ class DbQuotaDriver(object):
if user_id is None:
user_id = context.user_id
db.reservation_commit(context, reservations, project_id=project_id,
user_id=user_id)
db.reservation_commit(
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.
:param context: The request context, for access checks.
@ -512,8 +501,9 @@ class DbQuotaDriver(object):
if user_id is None:
user_id = context.user_id
db.reservation_rollback(context, reservations, project_id=project_id,
user_id=user_id)
db.reservation_rollback(
context, reservations, project_id=project_id, user_id=user_id,
share_type_id=share_type_id)
def usage_reset(self, context, resources):
"""Reset usage records.
@ -571,6 +561,21 @@ class DbQuotaDriver(object):
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):
"""Expire reservations.
@ -598,54 +603,6 @@ class BaseResource(object):
self.name = name
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
def default(self):
"""Return the default value of the quota."""
@ -768,17 +725,6 @@ class QuotaEngine(object):
for resource in resources:
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):
"""Get a specific quota by quota class."""
@ -830,6 +776,28 @@ class QuotaEngine(object):
defaults=defaults,
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,
defaults=True, usages=True, remains=False):
"""Retrieve the quotas for the given project.
@ -856,7 +824,8 @@ class QuotaEngine(object):
usages=usages,
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.
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 project_id: The ID of the project 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,
project_id,
user_id=user_id)
return self._driver.get_settable_quotas(
context, self._resources, project_id, user_id=user_id,
share_type_id=share_type_id)
def count(self, context, resource, *args, **kwargs):
"""Count a resource.
@ -891,40 +861,8 @@ class QuotaEngine(object):
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,
**deltas):
share_type_id=None, **deltas):
"""Check quotas and reserve resources.
For counting quotas--those quotas for which there is a usage
@ -959,16 +897,20 @@ class QuotaEngine(object):
common user's tenant.
"""
reservations = self._driver.reserve(context, self._resources, deltas,
expire=expire,
project_id=project_id,
user_id=user_id)
reservations = self._driver.reserve(
context, self._resources, deltas,
expire=expire,
project_id=project_id,
user_id=user_id,
share_type_id=share_type_id,
)
LOG.debug("Created reservations %s", 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.
:param context: The request context, for access checks.
@ -980,8 +922,9 @@ class QuotaEngine(object):
"""
try:
self._driver.commit(context, reservations, project_id=project_id,
user_id=user_id)
self._driver.commit(
context, reservations, project_id=project_id,
user_id=user_id, share_type_id=share_type_id)
except Exception:
# NOTE(Vek): Ignoring exceptions here is safe, because the
# usage resynchronization and the reservation expiration
@ -992,7 +935,8 @@ class QuotaEngine(object):
return
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.
:param context: The request context, for access checks.
@ -1004,8 +948,9 @@ class QuotaEngine(object):
"""
try:
self._driver.rollback(context, reservations, project_id=project_id,
user_id=user_id)
self._driver.rollback(
context, reservations, project_id=project_id,
user_id=user_id, share_type_id=share_type_id)
except Exception:
# NOTE(Vek): Ignoring exceptions here is safe, because the
# usage resynchronization and the reservation expiration
@ -1048,6 +993,21 @@ class QuotaEngine(object):
self._driver.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.
"""
self._driver.destroy_all_by_project_and_share_type(
context, project_id, share_type_id)
def destroy_all_by_project(self, context, project_id):
"""Destroy metadata associated with a project.

View File

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

View File

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

View File

@ -15,7 +15,7 @@
# under the License.
"""
Tests for manila.api.v1.quota_sets.py
Tests for manila.api.v2.quota_sets.py
"""
import copy
@ -37,7 +37,7 @@ from manila import utils
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'].is_admin = True
REQ.environ['manila.context'].auth_token = 'foo_auth_token'
@ -122,6 +122,160 @@ class QuotaSetsControllerTest(test.TestCase):
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)
def test_quota_detail(self, request):
request.api_version_request = api_version.APIVersionRequest('2.25')
@ -207,6 +361,10 @@ class QuotaSetsControllerTest(test.TestCase):
@ddt.data(REQ, REQ_WITH_USER)
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)
body = {'quota_set': {'tenant_id': self.project_id, 'shares': 788}}
expected = {
@ -234,6 +392,79 @@ class QuotaSetsControllerTest(test.TestCase):
self.assertEqual(expected, show_result)
self.mock_policy_check.assert_has_calls([
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])
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,
'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):
self.assertRaises(
webob.exc.HTTPForbidden,

View File

@ -2396,3 +2396,73 @@ class MessagesTableChecks(BaseMigrationChecks):
def check_downgrade(self, engine):
self.test_case.assertRaises(sa_exc.NoSuchTableError, utils.load_table,
'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,
'share_group_id': None,
'instance': {
'id': 'fake_share_instance_id',
'host': 'fakehost',
'share_type_id': '1',
},
@ -61,6 +62,7 @@ def fake_share_instance(base_share=None, **kwargs):
'host': 'fakehost',
'share_network_id': 'fakesharenetworkid',
'share_server_id': 'fakeshareserverid',
'share_type_id': '1',
}
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',
'share_name': 'fakename',
'share_id': 'fakeshareinstanceid',
'share_instance': {'share_id': 'fakeshareid', },
'share_instance': {
'share_id': 'fakeshareid',
'share_type_id': '1',
},
'share_instance_id': 'fakeshareinstanceid',
'deleted': False,
'updated_at': datetime.datetime(2016, 3, 21, 0, 5, 58),

View File

@ -677,7 +677,8 @@ class ShareAPITestCase(test.TestCase):
share_data['display_description']
)
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)
def test_create_share_error_on_quota_commit(self, expected_exception):
@ -700,8 +701,8 @@ class ShareAPITestCase(test.TestCase):
share_data['display_description']
)
quota.QUOTAS.rollback.assert_called_once_with(self.context,
reservation)
quota.QUOTAS.rollback.assert_called_once_with(
self.context, reservation, share_type_id=None)
db_api.share_delete.assert_called_once_with(self.context, share['id'])
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(
self.context, 'share', 'create_snapshot', share)
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(
self.context, 'reservation')
self.context, 'reservation', share_type_id=None)
db_api.share_snapshot_create.assert_called_once_with(
self.context, options)
@ -1248,7 +1250,8 @@ class ShareAPITestCase(test.TestCase):
if reservations is not None:
mock_quotas_rollback.assert_called_once_with(
self.context, reservations)
self.context, reservations,
share_type_id=share['instance']['share_type_id'])
else:
self.assertFalse(mock_quotas_rollback.called)
@ -1284,6 +1287,7 @@ class ShareAPITestCase(test.TestCase):
self.assertEqual('fake_reservations', result)
mock_quotas_reserve.assert_called_once_with(
self.context, project_id='fake_project', gigabytes=1,
share_type_id=share['instance']['share_type_id'],
user_id='fake_user')
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_snapshot', 'get_snapshot')])
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(
self.context, 'reservation')
self.context, 'reservation', share_type_id=share_type['id'])
def test_create_from_snapshot_with_different_share_type(self):
snapshot, share, share_data, request_spec = (
@ -1734,12 +1739,14 @@ class ShareAPITestCase(test.TestCase):
project_id=share['project_id'],
shares=-1,
gigabytes=-share['size'],
share_type_id=None,
user_id=share['user_id']
)
quota.QUOTAS.commit.assert_called_once_with(
diff_user_context,
mock.ANY,
project_id=share['project_id'],
share_type_id=None,
user_id=share['user_id']
)
@ -1810,6 +1817,7 @@ class ShareAPITestCase(test.TestCase):
project_id=share['project_id'],
shares=-1,
gigabytes=-share['size'],
share_type_id=None,
user_id=share['user_id']
)
self.assertFalse(quota.QUOTAS.commit.called)
@ -2239,6 +2247,7 @@ class ShareAPITestCase(test.TestCase):
diff_user_context,
project_id=share['project_id'],
gigabytes=size_increase,
share_type_id=None,
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):
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(
share_id=share_id, share=share, name='fake_snapshot')
snapshot = fakes.fake_snapshot(
@ -1648,7 +1648,8 @@ class ShareManagerTestCase(test.TestCase):
self.assertTrue(manager.QUOTAS.reserve.called)
quota.QUOTAS.reserve.assert_called_once_with(
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(quota_error, mock_exception_log.called)
self.assertEqual(expected_exc_count, mock_exception_log.call_count)
@ -1660,7 +1661,7 @@ class ShareManagerTestCase(test.TestCase):
raise exception.QuotaError(code='500')
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(
share_id=share_id, share=share, name='fake_snapshot')
snapshot = fakes.fake_snapshot(
@ -3038,7 +3039,8 @@ class ShareManagerTestCase(test.TestCase):
mock.ANY,
reservations,
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')
@ -3076,7 +3078,7 @@ class ShareManagerTestCase(test.TestCase):
)
quota.QUOTAS.commit.assert_called_once_with(
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(
mock.ANY, share_id, shr_update
)
@ -3103,6 +3105,7 @@ class ShareManagerTestCase(test.TestCase):
mock.ANY,
project_id=six.text_type(share['project_id']),
user_id=six.text_type(share['user_id']),
share_type_id=None,
gigabytes=new_size - size
)
self.assertTrue(self.share_manager.db.share_update.called)
@ -3140,11 +3143,11 @@ class ShareManagerTestCase(test.TestCase):
)
quota.QUOTAS.reserve.assert_called_once_with(
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(
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)
@ -3184,11 +3187,11 @@ class ShareManagerTestCase(test.TestCase):
quota.QUOTAS.reserve.assert_called_once_with(
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(
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(
mock.ANY, share_id, shr_update
@ -5508,10 +5511,12 @@ class ShareManagerTestCase(test.TestCase):
share_id = 'fake_share_id'
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)
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(
id='fake_snapshot_id', share_id=share_id, share=share,
instance=snapshot_instance, project_id='fake_project',
@ -5543,7 +5548,9 @@ class ShareManagerTestCase(test.TestCase):
if reservations:
mock_quotas_commit.assert_called_once_with(
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:
self.assertFalse(mock_quotas_commit.called)
@ -5567,10 +5574,12 @@ class ShareManagerTestCase(test.TestCase):
share_id = 'fake_share_id'
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)
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(
id='fake_snapshot_id', share_id=share_id, share=share,
instance=snapshot_instance, project_id='fake_project',
@ -5607,7 +5616,9 @@ class ShareManagerTestCase(test.TestCase):
if reservations:
mock_quotas_rollback.assert_called_once_with(
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:
self.assertFalse(mock_quotas_rollback.called)
@ -5822,7 +5833,7 @@ class ShareManagerTestCase(test.TestCase):
if reservations:
mock_quotas_commit.assert_called_once_with(
mock.ANY, reservations, project_id='fake_project',
user_id='fake_user')
user_id='fake_user', share_type_id=None)
else:
self.assertFalse(mock_quotas_commit.called)
@ -5851,10 +5862,12 @@ class ShareManagerTestCase(test.TestCase):
snapshot_instances = [snapshot['instance'], snapshot_instance]
active_replica = fake_replica(
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(
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]
access_rules = []
self.mock_object(
@ -5893,7 +5906,7 @@ class ShareManagerTestCase(test.TestCase):
if reservations:
mock_quotas_rollback.assert_called_once_with(
mock.ANY, reservations, project_id='fake_project',
user_id='fake_user')
user_id='fake_user', share_type_id=replica['share_type_id'])
else:
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 "
"value of the minimum microversion supported by Manila."),
cfg.StrOpt("max_api_microversion",
default="2.38",
default="2.39",
help="The maximum api microversion is configured to be the "
"value of the latest microversion supported by Manila."),
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"):
return '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):
if url is None:
url = self._get_quotas_url(version)
@ -865,48 +877,44 @@ class SharesV2Client(shares_client.SharesClient):
self.expected_success(200, resp.status)
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):
if url is None:
url = self._get_quotas_url(version)
url += '/%s' % tenant_id
if user_id is not None:
url += "?user_id=%s" % user_id
url += self._get_quotas_url_arguments_as_str(user_id, share_type)
resp, body = self.get(url, version=version)
self.expected_success(200, resp.status)
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):
if url is None:
url = self._get_quotas_url(version)
url += '/%s' % tenant_id
if user_id is not None:
url += "?user_id=%s" % user_id
url += self._get_quotas_url_arguments_as_str(user_id, share_type)
resp, body = self.delete(url, version=version)
self.expected_success(202, resp.status)
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):
if url is None:
url = self._get_quotas_url(version)
url += '/%s/detail' % tenant_id
if user_id is not None:
url += "?user_id=%s" % user_id
url += self._get_quotas_url_arguments_as_str(user_id, share_type)
resp, body = self.get(url, version=version)
self.expected_success(200, resp.status)
return self._parse_resp(body)
def update_quotas(self, tenant_id, user_id=None, shares=None,
snapshots=None, gigabytes=None, snapshot_gigabytes=None,
share_networks=None, force=True, url=None,
version=LATEST_MICROVERSION):
share_networks=None, force=True, share_type=None,
url=None, version=LATEST_MICROVERSION):
if url is None:
url = self._get_quotas_url(version)
url += '/%s' % tenant_id
if user_id is not None:
url += "?user_id=%s" % user_id
url += self._get_quotas_url_arguments_as_str(user_id, share_type)
put_body = {"tenant_id": tenant_id}
if force:

View File

@ -13,7 +13,10 @@
# License for the specific language governing permissions and limitations
# under the License.
import ddt
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 manila_tempest_tests.tests.api import base
@ -21,6 +24,7 @@ from manila_tempest_tests.tests.api import base
CONF = config.CONF
@ddt.ddt
class SharesAdminQuotasTest(base.BaseSharesAdminTest):
@classmethod
@ -60,7 +64,37 @@ class SharesAdminQuotasTest(base.BaseSharesAdminTest):
self.assertGreater(int(quotas["snapshots"]), -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):
force_tenant_isolation = True
@ -101,6 +135,47 @@ class SharesAdminQuotasUpdateTest(base.BaseSharesAdminTest):
self.tenant_id, self.user_id, shares=new_quota)
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)
def test_update_tenant_quota_snapshots(self):
# get current quotas
@ -244,6 +319,51 @@ class SharesAdminQuotasUpdateTest(base.BaseSharesAdminTest):
self.assertEqual(int(default["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)
def test_unlimited_quota_for_shares(self):
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)
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
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
@ -117,6 +118,7 @@ class SharesAdminQuotasNegativeTest(base.BaseSharesAdminTest):
client.update_quotas,
client.tenant_id,
client.user_id,
force=False,
shares=bigger_value)
@tc.attr(base.TAG_NEGATIVE, base.TAG_API)
@ -132,6 +134,7 @@ class SharesAdminQuotasNegativeTest(base.BaseSharesAdminTest):
client.update_quotas,
client.tenant_id,
client.user_id,
force=False,
snapshots=bigger_value)
@tc.attr(base.TAG_NEGATIVE, base.TAG_API)
@ -147,6 +150,7 @@ class SharesAdminQuotasNegativeTest(base.BaseSharesAdminTest):
client.update_quotas,
client.tenant_id,
client.user_id,
force=False,
gigabytes=bigger_value)
@tc.attr(base.TAG_NEGATIVE, base.TAG_API)
@ -162,6 +166,7 @@ class SharesAdminQuotasNegativeTest(base.BaseSharesAdminTest):
client.update_quotas,
client.tenant_id,
client.user_id,
force=False,
snapshot_gigabytes=bigger_value)
@tc.attr(base.TAG_NEGATIVE, base.TAG_API)
@ -177,6 +182,7 @@ class SharesAdminQuotasNegativeTest(base.BaseSharesAdminTest):
client.update_quotas,
client.tenant_id,
client.user_id,
force=False,
share_networks=bigger_value)
@ddt.data(
@ -215,3 +221,98 @@ class SharesAdminQuotasNegativeTest(base.BaseSharesAdminTest):
self.shares_v2_client.tenant_id,
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.