diff --git a/manila/api/openstack/api_version_request.py b/manila/api/openstack/api_version_request.py index 3ea711c4bb..9f488f4feb 100644 --- a/manila/api/openstack/api_version_request.py +++ b/manila/api/openstack/api_version_request.py @@ -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 diff --git a/manila/api/openstack/rest_api_version_history.rst b/manila/api/openstack/rest_api_version_history.rst index 8948248ec6..78aeaca420 100644 --- a/manila/api/openstack/rest_api_version_history.rst +++ b/manila/api/openstack/rest_api_version_history.rst @@ -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. diff --git a/manila/api/v2/quota_sets.py b/manila/api/v2/quota_sets.py index f11b059f54..cdcd3e7c89 100644 --- a/manila/api/v2/quota_sets.py +++ b/manila/api/v2/quota_sets.py @@ -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) diff --git a/manila/api/views/quota_sets.py b/manila/api/views/quota_sets.py index 2057733803..4d89bf78f7 100644 --- a/manila/api/views/quota_sets.py +++ b/manila/api/views/quota_sets.py @@ -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} diff --git a/manila/db/api.py b/manila/db/api.py index f4426144bd..364988ff26 100644 --- a/manila/db/api.py +++ b/manila/db/api.py @@ -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, + ) + ################## diff --git a/manila/db/migrations/alembic/versions/7d142971c4ef_add_reservation_expire_index.py b/manila/db/migrations/alembic/versions/7d142971c4ef_add_reservation_expire_index.py index 6a1a2b9a0f..27d253630d 100644 --- a/manila/db/migrations/alembic/versions/7d142971c4ef_add_reservation_expire_index.py +++ b/manila/db/migrations/alembic/versions/7d142971c4ef_add_reservation_expire_index.py @@ -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) diff --git a/manila/db/migrations/alembic/versions/b516de97bfee_add_quota_per_share_type_model.py b/manila/db/migrations/alembic/versions/b516de97bfee_add_quota_per_share_type_model.py new file mode 100644 index 0000000000..52a246c9dd --- /dev/null +++ b/manila/db/migrations/alembic/versions/b516de97bfee_add_quota_per_share_type_model.py @@ -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') diff --git a/manila/db/sqlalchemy/api.py b/manila/db/sqlalchemy/api.py index 17dabe3463..1ecc14af90 100644 --- a/manila/db/sqlalchemy/api.py +++ b/manila/db/sqlalchemy/api.py @@ -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] + + ################### diff --git a/manila/db/sqlalchemy/models.py b/manila/db/sqlalchemy/models.py index 4be97680dc..47d26e98a7 100644 --- a/manila/db/sqlalchemy/models.py +++ b/manila/db/sqlalchemy/models.py @@ -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) diff --git a/manila/exception.py b/manila/exception.py index f0d1ae8eca..dad14db505 100644 --- a/manila/exception.py +++ b/manila/exception.py @@ -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): diff --git a/manila/quota.py b/manila/quota.py index 26123395f9..934eb775a7 100644 --- a/manila/quota.py +++ b/manila/quota.py @@ -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. diff --git a/manila/share/api.py b/manila/share/api.py index 40a3a8b651..8dd76ffff0 100644 --- a/manila/share/api.py +++ b/manila/share/api.py @@ -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'] diff --git a/manila/share/manager.py b/manila/share/manager.py index ec6e47bcd4..251dbf828f 100644 --- a/manila/share/manager.py +++ b/manila/share/manager.py @@ -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, diff --git a/manila/tests/api/v2/test_quota_sets.py b/manila/tests/api/v2/test_quota_sets.py index f1f0268aa2..6d5be719e1 100644 --- a/manila/tests/api/v2/test_quota_sets.py +++ b/manila/tests/api/v2/test_quota_sets.py @@ -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, diff --git a/manila/tests/db/migrations/alembic/migrations_data_checks.py b/manila/tests/db/migrations/alembic/migrations_data_checks.py index 5e688fddc4..f3215be531 100644 --- a/manila/tests/db/migrations/alembic/migrations_data_checks.py +++ b/manila/tests/db/migrations/alembic/migrations_data_checks.py @@ -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')) diff --git a/manila/tests/fake_share.py b/manila/tests/fake_share.py index 91e7ce7056..130087d35d 100644 --- a/manila/tests/fake_share.py +++ b/manila/tests/fake_share.py @@ -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), diff --git a/manila/tests/share/test_api.py b/manila/tests/share/test_api.py index e31b7950ee..caea0075cf 100644 --- a/manila/tests/share/test_api.py +++ b/manila/tests/share/test_api.py @@ -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'] ) diff --git a/manila/tests/share/test_manager.py b/manila/tests/share/test_manager.py index 1ade84b04c..127d766555 100644 --- a/manila/tests/share/test_manager.py +++ b/manila/tests/share/test_manager.py @@ -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) diff --git a/manila/tests/test_quota.py b/manila/tests/test_quota.py index af28fcda0c..4a68f493ad 100644 --- a/manila/tests/test_quota.py +++ b/manila/tests/test_quota.py @@ -1,5 +1,4 @@ -# Copyright 2010 United States Government as represented by the -# Administrator of the National Aeronautics and Space Administration. +# Copyright 2017 Mirantis Inc. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may @@ -14,1526 +13,704 @@ # License for the specific language governing permissions and limitations # under the License. -import datetime - +import ddt import mock from oslo_config import cfg -from oslo_utils import timeutils -import testtools -from manila.common import constants -from manila import context -from manila import db -from manila.db.sqlalchemy import api as sqa_api -from manila.db.sqlalchemy import models as sqa_models from manila import exception from manila import quota -from manila import share from manila import test -from manila.tests import db_utils CONF = cfg.CONF -class QuotaIntegrationTestCase(test.TestCase): +@ddt.ddt +class DbQuotaDriverTestCase(test.TestCase): + def setUp(self): - super(QuotaIntegrationTestCase, self).setUp() - self.flags(quota_shares=2, - quota_gigabytes=20) - - self.user_id = 'admin' - self.project_id = 'admin' - self.create_share = lambda size=10: ( - db_utils.create_share(user_id=self.user_id, - project_id=self.project_id, - size=size, - status=constants.STATUS_AVAILABLE) - ) - self.context = context.RequestContext(self.user_id, - self.project_id, - is_admin=True) - - @testtools.skip("SQLAlchemy sqlite insert bug") - def test_too_many_shares(self): - share_ids = [] - for i in range(CONF.quota_shares): - share_ref = self.create_share() - share_ids.append(share_ref['id']) - self.assertRaises(exception.QuotaError, - share.API().create, - self.context, 'nfs', 10, '', '', None) - for share_id in share_ids: - db.share_delete(self.context, share_id) - - @testtools.skip("SQLAlchemy sqlite insert bug") - def test_too_many_gigabytes(self): - share_ids = [] - share_ref = self.create_share(size=20) - share_ids.append(share_ref['id']) - self.assertRaises(exception.QuotaError, - share.API().create, - self.context, 'cifs', 10, '', '', None) - for share_id in share_ids: - db.share_delete(self.context, share_id) - - -class FakeContext(object): - def __init__(self, project_id, quota_class): - self.is_admin = False - self.user_id = 'fake_user' - self.project_id = project_id - self.quota_class = quota_class - self.read_deleted = 'no' - - def elevated(self): - elevated = self.__class__(self.project_id, self.quota_class) - elevated.is_admin = True - return elevated - - -class FakeDriver(object): - def __init__(self, by_project=None, by_class=None, reservations=None): - self.called = [] - self.by_project = by_project or {} - self.by_class = by_class or {} - self.reservations = reservations or [] - - def get_by_project(self, context, project_id, resource): - self.called.append(('get_by_project', context, project_id, resource)) - try: - return self.by_project[project_id][resource] - except KeyError: - raise exception.ProjectQuotaNotFound(project_id=project_id) - - def get_by_class(self, context, quota_class, resource): - self.called.append(('get_by_class', context, quota_class, resource)) - try: - return self.by_class[quota_class][resource] - except KeyError: - raise exception.QuotaClassNotFound(class_name=quota_class) - - def get_defaults(self, context, resources): - self.called.append(('get_defaults', context, resources)) - return resources - - def get_class_quotas(self, context, resources, quota_class, - defaults=True): - self.called.append(('get_class_quotas', context, resources, - quota_class, defaults)) - return resources - - def get_project_quotas(self, context, resources, project_id, - quota_class=None, defaults=True, usages=True, - remains=False): - self.called.append(('get_project_quotas', context, resources, - project_id, quota_class, defaults, usages, - remains)) - return resources - - def limit_check(self, context, resources, values, project_id=None, - user_id=None): - self.called.append(('limit_check', context, resources, - values, project_id, user_id)) - - def reserve(self, context, resources, deltas, expire=None, - project_id=None, user_id=None): - self.called.append(('reserve', context, resources, deltas, - expire, project_id, user_id)) - return self.reservations - - def commit(self, context, reservations, project_id=None, user_id=None): - self.called.append(('commit', context, reservations, project_id, - user_id)) - - def rollback(self, context, reservations, project_id=None, user_id=None): - self.called.append(('rollback', context, reservations, project_id, - user_id)) - - def destroy_all_by_project_and_user(self, context, project_id, user_id): - self.called.append(('destroy_all_by_project_and_user', context, - project_id, user_id)) - - def destroy_all_by_project(self, context, project_id): - self.called.append(('destroy_all_by_project', context, project_id)) - - def expire(self, context): - self.called.append(('expire', context)) - - -class BaseResourceTestCase(test.TestCase): - def test_no_flag(self): - resource = quota.BaseResource('test_resource') - - self.assertEqual('test_resource', resource.name) - self.assertIsNone(resource.flag) - self.assertEqual(-1, resource.default) - - def test_with_flag(self): - # We know this flag exists, so use it... - self.flags(quota_shares=10) - resource = quota.BaseResource('test_resource', 'quota_shares') - - self.assertEqual('test_resource', resource.name) - self.assertEqual('quota_shares', resource.flag) - self.assertEqual(10, resource.default) - - def test_with_flag_no_quota(self): - self.flags(quota_shares=-1) - resource = quota.BaseResource('test_resource', 'quota_shares') - - self.assertEqual('test_resource', resource.name) - self.assertEqual('quota_shares', resource.flag) - self.assertEqual(-1, resource.default) - - def test_quota_no_project_no_class(self): - self.flags(quota_shares=10) - resource = quota.BaseResource('test_resource', 'quota_shares') - driver = FakeDriver() - context = FakeContext(None, None) - quota_value = resource.quota(driver, context) - - self.assertEqual(10, quota_value) - - def test_quota_with_project_no_class(self): - self.flags(quota_shares=10) - resource = quota.BaseResource('test_resource', 'quota_shares') - driver = FakeDriver( - by_project=dict( - test_project=dict(test_resource=15), )) - context = FakeContext('test_project', None) - quota_value = resource.quota(driver, context) - - self.assertEqual(15, quota_value) - - def test_quota_no_project_with_class(self): - self.flags(quota_shares=10) - resource = quota.BaseResource('test_resource', 'quota_shares') - driver = FakeDriver( - by_class=dict( - test_class=dict(test_resource=20), )) - context = FakeContext(None, 'test_class') - quota_value = resource.quota(driver, context) - - self.assertEqual(20, quota_value) - - def test_quota_with_project_with_class(self): - self.flags(quota_shares=10) - resource = quota.BaseResource('test_resource', 'quota_shares') - driver = FakeDriver(by_project=dict( - test_project=dict(test_resource=15), ), - by_class=dict(test_class=dict(test_resource=20), )) - context = FakeContext('test_project', 'test_class') - quota_value = resource.quota(driver, context) - - self.assertEqual(15, quota_value) - - def test_quota_override_project_with_class(self): - self.flags(quota_shares=10) - resource = quota.BaseResource('test_resource', 'quota_shares') - driver = FakeDriver(by_project=dict( - test_project=dict(test_resource=15), - override_project=dict(test_resource=20), )) - context = FakeContext('test_project', 'test_class') - quota_value = resource.quota(driver, context, - project_id='override_project') - - self.assertEqual(20, quota_value) - - def test_quota_with_project_override_class(self): - self.flags(quota_shares=10) - resource = quota.BaseResource('test_resource', 'quota_shares') - driver = FakeDriver(by_class=dict( - test_class=dict(test_resource=15), - override_class=dict(test_resource=20), )) - context = FakeContext('test_project', 'test_class') - quota_value = resource.quota(driver, context, - quota_class='override_class') - - self.assertEqual(20, quota_value) - - -class QuotaEngineTestCase(test.TestCase): - def test_init(self): - quota_obj = quota.QuotaEngine() - - self.assertEqual({}, quota_obj._resources) - self.assertIsInstance(quota_obj._driver, quota.DbQuotaDriver) - - def test_init_override_string(self): - quota_obj = quota.QuotaEngine( - quota_driver_class='manila.tests.test_quota.FakeDriver') - - self.assertEqual({}, quota_obj._resources) - self.assertIsInstance(quota_obj._driver, FakeDriver) - - def test_init_override_obj(self): - quota_obj = quota.QuotaEngine(quota_driver_class=FakeDriver) - - self.assertEqual({}, quota_obj._resources) - self.assertEqual(FakeDriver, quota_obj._driver) - - def test_register_resource(self): - quota_obj = quota.QuotaEngine() - resource = quota.AbsoluteResource('test_resource') - quota_obj.register_resource(resource) - - self.assertEqual(dict(test_resource=resource), quota_obj._resources) - - def test_register_resources(self): - quota_obj = quota.QuotaEngine() - resources = [ - quota.AbsoluteResource('test_resource1'), - quota.AbsoluteResource('test_resource2'), - quota.AbsoluteResource('test_resource3'), ] - quota_obj.register_resources(resources) - - self.assertEqual(dict(test_resource1=resources[0], - test_resource2=resources[1], - test_resource3=resources[2], ), - quota_obj._resources) - - def test_sync_predeclared(self): - quota_obj = quota.QuotaEngine() - - def spam(*args, **kwargs): - pass - - resource = quota.ReservableResource('test_resource', spam) - quota_obj.register_resource(resource) - - self.assertEqual(spam, resource.sync) - - def test_sync_multi(self): - quota_obj = quota.QuotaEngine() - - def spam(*args, **kwargs): - pass - - resources = [ - quota.ReservableResource('test_resource1', spam), - quota.ReservableResource('test_resource2', spam), - quota.ReservableResource('test_resource3', spam), - quota.ReservableResource('test_resource4', spam), ] - quota_obj.register_resources(resources[:2]) - - self.assertEqual(spam, resources[0].sync) - self.assertEqual(spam, resources[1].sync) - self.assertEqual(spam, resources[2].sync) - self.assertEqual(spam, resources[3].sync) - - def test_get_by_project(self): - context = FakeContext('test_project', 'test_class') - driver = FakeDriver( - by_project=dict( - test_project=dict(test_resource=42))) - quota_obj = quota.QuotaEngine(quota_driver_class=driver) - result = quota_obj.get_by_project(context, 'test_project', - 'test_resource') - - self.assertEqual([('get_by_project', - context, - 'test_project', - 'test_resource'), ], driver.called) - self.assertEqual(42, result) + super(self.__class__, self).setUp() + self.project_id = 'fake_project_id' + self.user_id = 'fake_user_id' + self.share_type_id = 'fake_share_type_id' + self.ctxt = type( + 'FakeContext', (object, ), + {'project_id': self.project_id, 'user_id': self.user_id, + 'quota_class': 'fake_quota_class', 'elevated': mock.Mock()}) + self.driver = quota.DbQuotaDriver() + self.reservations = ['foo', 'bar'] + self.resources = {k: quota.BaseResource(k) for k in ('foo', 'bar')} def test_get_by_class(self): - context = FakeContext('test_project', 'test_class') - driver = FakeDriver( - by_class=dict( - test_class=dict(test_resource=42))) - quota_obj = quota.QuotaEngine(quota_driver_class=driver) - result = quota_obj.get_by_class(context, 'test_class', 'test_resource') + self.mock_object(quota.db, 'quota_class_get') - self.assertEqual([('get_by_class', - context, - 'test_class', - 'test_resource'), ], driver.called) - self.assertEqual(42, result) + result = self.driver.get_by_class( + self.ctxt, 'fake_quota_class', 'fake_res') - def _make_quota_obj(self, driver): - quota_obj = quota.QuotaEngine(quota_driver_class=driver) - resources = [ - quota.AbsoluteResource('test_resource4'), - quota.AbsoluteResource('test_resource3'), - quota.AbsoluteResource('test_resource2'), - quota.AbsoluteResource('test_resource1'), ] - quota_obj.register_resources(resources) - - return quota_obj + self.assertEqual(quota.db.quota_class_get.return_value, result) + quota.db.quota_class_get.assert_called_once_with( + self.ctxt, 'fake_quota_class', 'fake_res') def test_get_defaults(self): - context = FakeContext(None, None) - driver = FakeDriver() - quota_obj = self._make_quota_obj(driver) - result = quota_obj.get_defaults(context) + self.mock_object( + quota.db, 'quota_class_get_default', + mock.Mock(return_value={'foo': 13})) - self.assertEqual([('get_defaults', - context, - quota_obj._resources), ], - driver.called) - self.assertEqual(quota_obj._resources, result) + result = self.driver.get_defaults(self.ctxt, self.resources) - def test_get_class_quotas(self): - context = FakeContext(None, None) - driver = FakeDriver() - quota_obj = self._make_quota_obj(driver) - result1 = quota_obj.get_class_quotas(context, 'test_class') - result2 = quota_obj.get_class_quotas(context, 'test_class', False) + self.assertEqual( + {'foo': 13, 'bar': self.resources['bar'].default}, result) + quota.db.quota_class_get_default.assert_called_once_with(self.ctxt) - self.assertEqual([ - ('get_class_quotas', - context, - quota_obj._resources, - 'test_class', True), - ('get_class_quotas', - context, quota_obj._resources, - 'test_class', False), ], driver.called) - self.assertEqual(quota_obj._resources, result1) - self.assertEqual(quota_obj._resources, result2) + @ddt.data(True, False) + def test_get_class_quotas(self, defaults): + self.mock_object( + quota.db, 'quota_class_get_all_by_name', + mock.Mock(return_value={'foo': 13})) - def test_get_project_quotas(self): - context = FakeContext(None, None) - driver = FakeDriver() - quota_obj = self._make_quota_obj(driver) - result1 = quota_obj.get_project_quotas(context, 'test_project') - result2 = quota_obj.get_project_quotas(context, 'test_project', - quota_class='test_class', - defaults=False, - usages=False) + result = self.driver.get_class_quotas( + self.ctxt, self.resources, 'fake_quota_class', defaults) - self.assertEqual([ - ('get_project_quotas', - context, - quota_obj._resources, - 'test_project', - None, - True, - True, - False), - ('get_project_quotas', - context, - quota_obj._resources, - 'test_project', - 'test_class', - False, - False, - False), ], - driver.called) - self.assertEqual(quota_obj._resources, result1) - self.assertEqual(quota_obj._resources, result2) + expected = {'foo': 13, 'bar': -1} if defaults else {'foo': 13} + self.assertEqual(expected, result) + quota.db.quota_class_get_all_by_name.assert_called_once_with( + self.ctxt, 'fake_quota_class') - def test_count_no_resource(self): - context = FakeContext(None, None) - driver = FakeDriver() - quota_obj = self._make_quota_obj(driver) - self.assertRaises(exception.QuotaResourceUnknown, - quota_obj.count, context, 'test_resource5', - True, foo='bar') + @ddt.data( + ('fake_project_id', {'foo': 20}, None, True, None, True), + ('fake_different_project_id', {'bar': 40}, 'fake_quota_class', True, + {'foo': {'in_use': 3, 'reserved': 2}}, False), + ('fake_project_id', {'bar': 30}, 'fake_quota_class', True, None, False) + ) + @ddt.unpack + def test__process_quotas(self, project_id, quotas, quota_class, defaults, + usages, remains): + self.mock_object(quota.db, 'quota_get_all', mock.Mock(return_value=[])) + self.mock_object(quota.db, 'quota_class_get_all_by_name') + self.mock_object( + self.driver, 'get_defaults', + mock.Mock(return_value={'foo': 11, 'bar': 12})) + self.mock_object( + quota.db, 'quota_get_all', + mock.Mock(return_value=[])) - def test_count_wrong_resource(self): - context = FakeContext(None, None) - driver = FakeDriver() - quota_obj = self._make_quota_obj(driver) - self.assertRaises(exception.QuotaResourceUnknown, - quota_obj.count, context, 'test_resource1', - True, foo='bar') + result = self.driver._process_quotas( + self.ctxt, self.resources, project_id, quotas, quota_class, + defaults, usages, remains) - def test_count(self): - def fake_count(context, *args, **kwargs): - self.assertEqual((True,), args) - self.assertEqual(dict(foo='bar'), kwargs) - return 5 + expected = {key: {'limit': mock.ANY} for key in ('foo', 'bar')} + if usages: + for res in self.resources.values(): + usage = usages.get(res.name, {}) + expected[res.name].update( + in_use=usage.get('in_use', 0), + reserved=usage.get('reserved', 0)) + if remains: + quota.db.quota_get_all.assert_called_once_with( + self.ctxt, project_id) + for res in self.resources.values(): + expected[res.name]['remains'] = mock.ANY + else: + self.assertEqual(0, quota.db.quota_get_all.call_count) - context = FakeContext(None, None) - driver = FakeDriver() - quota_obj = self._make_quota_obj(driver) - quota_obj.register_resource(quota.CountableResource('test_resource5', - fake_count)) - result = quota_obj.count(context, 'test_resource5', True, foo='bar') + self.assertEqual(expected, result) + if quota_class or project_id == self.ctxt.project_id: + quota.db.quota_class_get_all_by_name.assert_called_once_with( + self.ctxt, quota_class or self.ctxt.quota_class) + else: + self.assertEqual( + 0, quota.db.quota_class_get_all_by_name.call_count) - self.assertEqual(5, result) + @ddt.data( + ('fake_quota_class', True, None, 'fake_remains'), + (None, False, 'fake_usages', False), + ) + @ddt.unpack + def test_get_project_quotas(self, quota_class, defaults, usages, remains): + self.mock_object(quota.db, 'quota_get_all_by_project') + self.mock_object(quota.db, 'quota_usage_get_all_by_project') + self.mock_object(self.driver, '_process_quotas') - def test_limit_check(self): - context = FakeContext(None, None) - driver = FakeDriver() - quota_obj = self._make_quota_obj(driver) - quota_obj.limit_check(context, test_resource1=4, test_resource2=3, - test_resource3=2, test_resource4=1) + result = self.driver.get_project_quotas( + self.ctxt, self.resources, self.project_id, + quota_class, defaults, usages, remains) - self.assertEqual([ - ('limit_check', - context, - quota_obj._resources, - dict( - test_resource1=4, - test_resource2=3, - test_resource3=2, - test_resource4=1,), - None, None), ], - driver.called) + self.assertEqual( + result, self.driver._process_quotas.return_value) + project_usages = None + if usages: + project_usages = ( + quota.db.quota_usage_get_all_by_project.return_value) + self.driver._process_quotas.assert_called_once_with( + self.ctxt, self.resources, self.project_id, + quota.db.quota_get_all_by_project.return_value, + quota_class, defaults=defaults, usages=project_usages, + remains=remains) + quota.db.quota_get_all_by_project.assert_called_once_with( + self.ctxt, self.project_id) + if usages: + quota.db.quota_usage_get_all_by_project.assert_called_once_with( + self.ctxt, self.project_id) + else: + self.assertEqual( + 0, quota.db.quota_usage_get_all_by_project.call_count) - def test_reserve(self): - context = FakeContext(None, None) - driver = FakeDriver(reservations=['resv-01', - 'resv-02', - 'resv-03', - 'resv-04', ]) - quota_obj = self._make_quota_obj(driver) - result1 = quota_obj.reserve(context, test_resource1=4, - test_resource2=3, test_resource3=2, - test_resource4=1) - result2 = quota_obj.reserve(context, expire=3600, - test_resource1=1, test_resource2=2, - test_resource3=3, test_resource4=4) - result3 = quota_obj.reserve(context, project_id='fake_project', - test_resource1=1, test_resource2=2, - test_resource3=3, test_resource4=4) + @ddt.data( + (None, True, True), + ('fake_quota_class', False, True), + ('fake_quota_class', True, False), + ) + @ddt.unpack + def test_get_user_quotas(self, quota_class, defaults, usages): + project_quotas = {'fake_resource': 5} + self.mock_object( + quota.db, 'quota_get_all_by_project', + mock.Mock(return_value=project_quotas)) + self.mock_object( + quota.db, 'quota_get_all_by_project_and_user', + mock.Mock(return_value={'fake_user_defined_resource': 14})) + mock_user_usages = self.mock_object( + quota.db, 'quota_usage_get_all_by_project_and_user') + self.mock_object(self.driver, '_process_quotas') - self.assertEqual([ - ('reserve', - context, - quota_obj._resources, - dict( - test_resource1=4, - test_resource2=3, - test_resource3=2, - test_resource4=1, ), - None, - None, - None), - ('reserve', - context, - quota_obj._resources, - dict( - test_resource1=1, - test_resource2=2, - test_resource3=3, - test_resource4=4, ), - 3600, - None, - None), - ('reserve', - context, - quota_obj._resources, - dict( - test_resource1=1, - test_resource2=2, - test_resource3=3, - test_resource4=4, ), - None, - 'fake_project', None), ], - driver.called) - self.assertEqual(['resv-01', - 'resv-02', - 'resv-03', - 'resv-04', ], result1) - self.assertEqual(['resv-01', - 'resv-02', - 'resv-03', - 'resv-04', ], result2) - self.assertEqual(['resv-01', - 'resv-02', - 'resv-03', - 'resv-04', ], result3) + result = self.driver.get_user_quotas( + self.ctxt, self.resources, self.project_id, self.user_id, + quota_class, defaults, usages) + + self.assertEqual( + self.driver._process_quotas.return_value, result) + quota.db.quota_get_all_by_project.assert_called_once_with( + self.ctxt, self.project_id) + quota.db.quota_get_all_by_project_and_user.assert_called_once_with( + self.ctxt, self.project_id, self.user_id) + if usages: + user_usages = mock_user_usages.return_value + mock_user_usages.assert_called_once_with( + self.ctxt, self.project_id, self.user_id) + else: + user_usages = None + self.assertEqual(0, mock_user_usages.call_count) + expected_user_quotas = {'fake_user_defined_resource': 14} + expected_user_quotas.update(project_quotas) + self.driver._process_quotas.assert_called_once_with( + self.ctxt, self.resources, self.project_id, expected_user_quotas, + quota_class, defaults=defaults, usages=user_usages) + + @ddt.data( + (None, True, True), + ('fake_quota_class', False, True), + ('fake_quota_class', True, False), + ) + @ddt.unpack + def test_get_share_type_quotas(self, quota_class, defaults, usages): + project_quotas = {'fake_resource': 5} + self.mock_object( + quota.db, 'quota_get_all_by_project', + mock.Mock(return_value=project_quotas)) + mock_st_quotas = self.mock_object( + quota.db, 'quota_get_all_by_project_and_share_type', + mock.Mock(return_value={'fake_st_defined_resource': 14})) + mock_st_usages = self.mock_object( + quota.db, 'quota_usage_get_all_by_project_and_share_type') + self.mock_object(self.driver, '_process_quotas') + + result = self.driver.get_share_type_quotas( + self.ctxt, self.resources, self.project_id, self.share_type_id, + quota_class, defaults, usages) + + self.assertEqual( + self.driver._process_quotas.return_value, result) + quota.db.quota_get_all_by_project.assert_called_once_with( + self.ctxt, self.project_id) + mock_st_quotas.assert_called_once_with( + self.ctxt, self.project_id, self.share_type_id) + if usages: + st_usages = mock_st_usages.return_value + mock_st_usages.assert_called_once_with( + self.ctxt, self.project_id, self.share_type_id) + else: + st_usages = None + self.assertEqual(0, mock_st_usages.call_count) + expected_st_quotas = {'fake_st_defined_resource': 14} + expected_st_quotas.update(project_quotas) + self.driver._process_quotas.assert_called_once_with( + self.ctxt, self.resources, self.project_id, expected_st_quotas, + quota_class, defaults=defaults, usages=st_usages) + + @ddt.data((None, None), (None, 'foo_st_id'), ('foo_user_id', None)) + @ddt.unpack + def test_get_settable_quotas(self, user_id, st_id): + project_quotas = {'fake': { + 'limit': 13, 'in_use': 7, 'reserved': 5, 'remains': 1, + }} + user_or_st_quotas = {'fake': { + 'limit': 11, 'in_use': 5, 'reserved': 2, + }} + self.mock_object( + self.driver, 'get_project_quotas', + mock.Mock(return_value=project_quotas)) + self.mock_object( + self.driver, 'get_user_quotas', + mock.Mock(return_value=user_or_st_quotas)) + self.mock_object( + self.driver, 'get_share_type_quotas', + mock.Mock(return_value=user_or_st_quotas)) + + result = self.driver.get_settable_quotas( + self.ctxt, self.resources, self.project_id, user_id, st_id) + + if user_id: + self.driver.get_user_quotas.assert_called_once_with( + self.ctxt, self.resources, self.project_id, user_id) + else: + self.assertEqual(0, self.driver.get_user_quotas.call_count) + if st_id: + self.driver.get_share_type_quotas.assert_called_once_with( + self.ctxt, self.resources, self.project_id, st_id) + else: + self.assertEqual(0, self.driver.get_share_type_quotas.call_count) + if user_id or st_id: + expected_settable_quotas = {'fake': {'maximum': 13, 'minimum': 7}} + else: + expected_settable_quotas = {'fake': {'maximum': -1, 'minimum': 12}} + self.driver.get_project_quotas.assert_called_once_with( + self.ctxt, self.resources, self.project_id, remains=True) + self.assertEqual(expected_settable_quotas, result) + + @ddt.data((None, None), (None, 'fake_st_id'), ('fake_user_id', None)) + @ddt.unpack + def test__get_quotas(self, user_id, st_id): + quotas = {'foo': {'limit': 5}, 'bar': {'limit': 13}} + self.mock_object( + self.driver, 'get_project_quotas', mock.Mock(return_value=quotas)) + self.mock_object( + self.driver, 'get_user_quotas', mock.Mock(return_value=quotas)) + self.mock_object( + self.driver, 'get_share_type_quotas', + mock.Mock(return_value=quotas)) + + result = self.driver._get_quotas( + self.ctxt, self.resources, ('foo', 'bar'), False, + self.project_id, user_id, st_id) + + expected = {k: v['limit'] for k, v in quotas.items()} + self.assertEqual(expected, result) + sub_resources = {k: v for k, v in self.resources.items()} + if user_id: + self.driver.get_user_quotas.assert_called_once_with( + self.ctxt, sub_resources, self.project_id, user_id, + self.ctxt.quota_class, usages=False) + self.assertEqual(0, self.driver.get_project_quotas.call_count) + self.assertEqual(0, self.driver.get_share_type_quotas.call_count) + elif st_id: + self.driver.get_share_type_quotas.assert_called_once_with( + self.ctxt, sub_resources, self.project_id, st_id, + self.ctxt.quota_class, usages=False) + self.assertEqual(0, self.driver.get_project_quotas.call_count) + self.assertEqual(0, self.driver.get_user_quotas.call_count) + else: + self.driver.get_project_quotas.assert_called_once_with( + self.ctxt, sub_resources, self.project_id, + self.ctxt.quota_class, usages=False) + self.assertEqual(0, self.driver.get_user_quotas.call_count) + self.assertEqual(0, self.driver.get_share_type_quotas.call_count) + + def test__get_quotas_unknown(self): + quotas = {'foo': {'limit': 5}, 'bar': {'limit': 13}} + self.mock_object( + self.driver, 'get_project_quotas', mock.Mock(return_value=quotas)) + self.mock_object( + self.driver, 'get_user_quotas', mock.Mock(return_value=quotas)) + self.mock_object( + self.driver, 'get_share_type_quotas', + mock.Mock(return_value=quotas)) + + self.assertRaises( + exception.QuotaResourceUnknown, + self.driver._get_quotas, + self.ctxt, self.resources, ['foo', 'bar'], True, + self.project_id, self.user_id, self.share_type_id) + + self.assertEqual(0, self.driver.get_project_quotas.call_count) + self.assertEqual(0, self.driver.get_user_quotas.call_count) + self.assertEqual(0, self.driver.get_share_type_quotas.call_count) + + @ddt.data( + {}, {'project_id': 'fake_project'}, {'user_id': 'fake_user'}, + {'share_type_id': 'fake_share_type_id'}, + ) + def test_reserve(self, kwargs): + self.mock_object(quota.db, 'quota_reserve') + deltas = {'delta1': 1, 'delta2': 2} + quotas, user_quotas, st_quotas = 'fake1', 'fake2', 'fake3' + self.mock_object( + self.driver, '_get_quotas', mock.Mock( + side_effect=[quotas, user_quotas, st_quotas])) + + result = self.driver.reserve( + self.ctxt, self.resources, deltas, None, **kwargs) + + expected_kwargs = { + 'project_id': self.ctxt.project_id, + 'user_id': self.ctxt.user_id, + 'share_type_id': None, + } + expected_kwargs.update(kwargs) + st_quotas = st_quotas if kwargs.get('share_type_id') else {} + self.assertEqual(quota.db.quota_reserve.return_value, result) + quota.db.quota_reserve.assert_called_once_with( + self.ctxt, self.resources, quotas, user_quotas, st_quotas, + deltas, mock.ANY, CONF.until_refresh, CONF.max_age, + **expected_kwargs) + self.assertEqual( + 3 if kwargs.get('share_type_id') else 2, + self.driver._get_quotas.call_count) + + def test_reserve_wrong_expire(self): + self.assertRaises( + exception.InvalidReservationExpiration, + self.driver.reserve, + self.ctxt, self.resources, 'fake_deltas', 'fake_expire') def test_commit(self): - context = FakeContext(None, None) - driver = FakeDriver() - quota_obj = self._make_quota_obj(driver) - quota_obj.commit(context, ['resv-01', 'resv-02', 'resv-03']) + self.mock_object(quota.db, 'reservation_commit') - self.assertEqual([('commit', - context, - ['resv-01', - 'resv-02', - 'resv-03'], - None, None), ], driver.called) + result = self.driver.commit( + self.ctxt, self.reservations, self.project_id, self.user_id, + self.share_type_id) - def test_rollback(self): - context = FakeContext(None, None) - driver = FakeDriver() - quota_obj = self._make_quota_obj(driver) - quota_obj.rollback(context, ['resv-01', 'resv-02', 'resv-03']) + self.assertIsNone(result) + quota.db.reservation_commit.assert_called_once_with( + self.ctxt, self.reservations, project_id=self.project_id, + user_id=self.user_id, share_type_id=self.share_type_id) - self.assertEqual([('rollback', - context, - ['resv-01', - 'resv-02', - 'resv-03'], - None, None), ], driver.called) + @ddt.data( + (None, None), + ('fake_project_id', 'fake_user_id'), + ) + @ddt.unpack + def test_rollback(self, project_id, user_id): + self.mock_object(quota.db, 'reservation_rollback') - def test_destroy_all_by_project_and_user(self): - context = FakeContext(None, None) - driver = FakeDriver() - quota_obj = self._make_quota_obj(driver) - quota_obj.destroy_all_by_project_and_user(context, - 'test_project', 'fake_user') + result = self.driver.rollback( + self.ctxt, self.reservations, project_id, user_id, + self.share_type_id) - self.assertEqual([ - ('destroy_all_by_project_and_user', context, 'test_project', - 'fake_user'), ], driver.called) + expected_project_id = project_id or self.ctxt.project_id + expected_user_id = user_id or self.ctxt.user_id + self.assertIsNone(result) + quota.db.reservation_rollback.assert_called_once_with( + self.ctxt, self.reservations, project_id=expected_project_id, + user_id=expected_user_id, share_type_id=self.share_type_id) + + def test_usage_reset(self): + self.mock_object( + quota.db, 'quota_usage_update', + mock.Mock(side_effect=[ + 'foo', + exception.QuotaUsageNotFound(project_id=self.project_id)])) + + result = self.driver.usage_reset(self.ctxt, ['foo', 'bar']) + + self.assertIsNone(result) + quota.db.quota_usage_update.assert_has_calls([ + mock.call( + self.ctxt.elevated.return_value, self.ctxt.project_id, + self.ctxt.user_id, res, in_use=-1) + for res in ('foo', 'bar') + ]) def test_destroy_all_by_project(self): - context = FakeContext(None, None) - driver = FakeDriver() - quota_obj = self._make_quota_obj(driver) - quota_obj.destroy_all_by_project(context, 'test_project') + self.mock_object(quota.db, 'quota_destroy_all_by_project') - self.assertEqual([('destroy_all_by_project', - context, - 'test_project'), ], driver.called) + result = self.driver.destroy_all_by_project(self.ctxt, self.project_id) + + self.assertIsNone(result) + quota.db.quota_destroy_all_by_project.assert_called_once_with( + self.ctxt, self.project_id) + + def test_destroy_all_by_project_and_user(self): + self.mock_object(quota.db, 'quota_destroy_all_by_project_and_user') + + result = self.driver.destroy_all_by_project_and_user( + self.ctxt, self.project_id, self.user_id) + + self.assertIsNone(result) + quota.db.quota_destroy_all_by_project_and_user.assert_called_once_with( + self.ctxt, self.project_id, self.user_id) + + def test_destroy_all_by_project_and_share_type(self): + mock_destroy_all = self.mock_object( + quota.db, 'quota_destroy_all_by_project_and_share_type') + + result = self.driver.destroy_all_by_project_and_share_type( + self.ctxt, self.project_id, self.share_type_id) + + self.assertIsNone(result) + mock_destroy_all.assert_called_once_with( + self.ctxt, self.project_id, self.share_type_id) def test_expire(self): - context = FakeContext(None, None) - driver = FakeDriver() - quota_obj = self._make_quota_obj(driver) - quota_obj.expire(context) + self.mock_object(quota.db, 'reservation_expire') - self.assertEqual([('expire', context), ], driver.called) + result = self.driver.expire(self.ctxt) - def test_resources(self): - quota_obj = self._make_quota_obj(None) - - self.assertEqual(['test_resource1', 'test_resource2', - 'test_resource3', 'test_resource4'], - quota_obj.resources) + self.assertIsNone(result) + quota.db.reservation_expire.assert_called_once_with(self.ctxt) -class DbQuotaDriverTestCase(test.TestCase): - expected_all_context = { - "shares": {"limit": 10, "in_use": 2, "reserved": 0, }, - "gigabytes": {"limit": 50, "in_use": 10, "reserved": 0, }, - "snapshot_gigabytes": {"limit": 50, "in_use": 20, "reserved": 0, }, - "snapshots": {"limit": 10, "in_use": 4, "reserved": 0, }, - "share_networks": {"limit": 10, "in_use": 0, "reserved": 0, }, - } +@ddt.ddt +class QuotaEngineTestCase(test.TestCase): def setUp(self): - super(DbQuotaDriverTestCase, self).setUp() - self.flags( - quota_shares=10, quota_snapshots=10, quota_gigabytes=1000, - quota_snapshot_gigabytes=1000, reservation_expire=86400, - until_refresh=0, max_age=0) + super(self.__class__, self).setUp() + self.ctxt = 'fake_context' + self.mock_class('manila.quota.DbQuotaDriver') + self.engine = quota.QuotaEngine() + self.driver = self.engine._driver + self.resources = [quota.BaseResource('foo'), quota.BaseResource('bar')] + self.project_id = 'fake_project_id' + self.user_id = 'fake_user_id' + self.share_type_id = 'fake_share_type_id' + self.quota_class = 'fake_quota_class' - self.driver = quota.DbQuotaDriver() + def test_register_resource(self): + self.assertNotIn(self.resources[0].name, self.engine) + self.engine.register_resource(self.resources[0]) + self.assertIn(self.resources[0].name, self.engine) - self.calls = [] + def test_register_resources(self): + for res in self.resources: + self.assertNotIn(res.name, self.engine) + self.engine.register_resources(self.resources) + for res in self.resources: + self.assertIn(res.name, self.engine) - self.patcher = mock.patch.object(timeutils, 'utcnow') - self.mock_utcnow = self.patcher.start() - self.mock_utcnow.return_value = datetime.datetime.utcnow() + def test_get_by_class(self): + result = self.engine.get_by_class( + self.ctxt, self.quota_class, 'fake_res') - def tearDown(self): - self.patcher.stop() - super(DbQuotaDriverTestCase, self).tearDown() + self.assertEqual(result, self.driver.get_by_class.return_value) + self.driver.get_by_class.assert_called_once_with( + self.ctxt, self.quota_class, 'fake_res') def test_get_defaults(self): - context = FakeContext('test_project', 'test_class') - # Use our pre-defined resources - result = self.driver.get_defaults(context, quota.QUOTAS._resources) - expected = { - "shares": 10, - "gigabytes": 1000, - "snapshot_gigabytes": 1000, - "snapshots": 10, - "share_networks": 10, + result = self.engine.get_defaults(self.ctxt) + + self.assertEqual(result, self.driver.get_defaults.return_value) + self.driver.get_defaults.assert_called_once_with( + self.ctxt, self.engine._resources) + + @ddt.data(None, True, False) + def test_get_class_quotas(self, defaults): + kwargs = {} + if defaults is not None: + kwargs['defaults'] = defaults + + result = self.engine.get_class_quotas( + self.ctxt, self.quota_class, **kwargs) + + self.assertEqual(result, self.driver.get_class_quotas.return_value) + kwargs['defaults'] = defaults if defaults is not None else True + self.driver.get_class_quotas.assert_called_once_with( + self.ctxt, self.engine._resources, self.quota_class, **kwargs) + + @ddt.data( + {}, + {'quota_class': 'foo'}, + {'defaults': False}, + {'usages': False}, + ) + def test_get_user_quotas(self, kwargs): + expected_kwargs = { + 'quota_class': None, + 'defaults': True, + 'usages': True, } - self.assertEqual(expected, result) + expected_kwargs.update(kwargs) - def _stub_quota_class_get_all_by_name(self): - # Stub out quota_class_get_all_by_name - def fake_qcgabn(context, quota_class): - self.calls.append('quota_class_get_all_by_name') - self.assertEqual('test_class', quota_class) - return dict(gigabytes=500, shares=10, snapshot_gigabytes=50) - self.mock_object(db, 'quota_class_get_all_by_name', fake_qcgabn) + result = self.engine.get_user_quotas( + self.ctxt, self.project_id, self.user_id, **kwargs) - def test_get_class_quotas(self): - self._stub_quota_class_get_all_by_name() - result = self.driver.get_class_quotas(None, quota.QUOTAS._resources, - 'test_class') + self.assertEqual(result, self.driver.get_user_quotas.return_value) + self.driver.get_user_quotas.assert_called_once_with( + self.ctxt, self.engine._resources, + self.project_id, self.user_id, **expected_kwargs) - self.assertEqual(['quota_class_get_all_by_name'], self.calls) - expected = { - "shares": 10, - "gigabytes": 500, - "snapshot_gigabytes": 50, - "snapshots": 10, - "share_networks": 10, + @ddt.data( + {}, + {'quota_class': 'foo'}, + {'defaults': False}, + {'usages': False}, + ) + def test_get_share_type_quotas(self, kwargs): + expected_kwargs = { + 'quota_class': None, + 'defaults': True, + 'usages': True, } - self.assertEqual(expected, result) + expected_kwargs.update(kwargs) - def test_get_class_quotas_no_defaults(self): - self._stub_quota_class_get_all_by_name() - result = self.driver.get_class_quotas(None, quota.QUOTAS._resources, - 'test_class', False) + result = self.engine.get_share_type_quotas( + self.ctxt, self.project_id, self.share_type_id, **kwargs) - self.assertEqual(['quota_class_get_all_by_name'], self.calls) self.assertEqual( - dict(shares=10, gigabytes=500, snapshot_gigabytes=50), result) + result, self.driver.get_share_type_quotas.return_value) + self.driver.get_share_type_quotas.assert_called_once_with( + self.ctxt, self.engine._resources, + self.project_id, self.share_type_id, **expected_kwargs) - def _stub_get_by_project_and_user(self): - def fake_qgabpu(context, project_id, user_id): - self.calls.append('quota_get_all_by_project_and_user') - self.assertEqual('test_project', project_id) - self.assertEqual('fake_user', user_id) - return dict( - shares=10, gigabytes=50, snapshots=10, snapshot_gigabytes=50, - reserved=0) - - def fake_qgabp(context, project_id): - self.calls.append('quota_get_all_by_project') - self.assertEqual('test_project', project_id) - return dict( - shares=10, gigabytes=50, snapshots=10, snapshot_gigabytes=50, - reserved=0) - - def fake_qugabpu(context, project_id, user_id): - self.calls.append('quota_usage_get_all_by_project_and_user') - self.assertEqual('test_project', project_id) - self.assertEqual('fake_user', user_id) - return dict( - shares=dict(in_use=2, reserved=0), - gigabytes=dict(in_use=10, reserved=0), - snapshots=dict(in_use=4, reserved=0), - snapshot_gigabytes=dict(in_use=20, reserved=0), - ) - - self.mock_object(db, 'quota_get_all_by_project_and_user', fake_qgabpu) - self.mock_object(db, 'quota_get_all_by_project', fake_qgabp) - self.mock_object(db, 'quota_usage_get_all_by_project_and_user', - fake_qugabpu) - - self._stub_quota_class_get_all_by_name() - - def test_get_user_quotas(self): - self._stub_get_by_project_and_user() - result = self.driver.get_user_quotas( - FakeContext('test_project', 'test_class'), - quota.QUOTAS._resources, 'test_project', 'fake_user') - - self.assertEqual([ - 'quota_get_all_by_project_and_user', - 'quota_get_all_by_project', - 'quota_usage_get_all_by_project_and_user', - 'quota_class_get_all_by_name', ], self.calls) - self.assertEqual(self.expected_all_context, result) - - def _stub_get_by_project(self): - def fake_qgabp(context, project_id): - self.calls.append('quota_get_all_by_project') - self.assertEqual('test_project', project_id) - return dict( - shares=10, gigabytes=50, snapshot_gigabytes=50, reserved=0) - - def fake_qugabp(context, project_id): - self.calls.append('quota_usage_get_all_by_project') - self.assertEqual('test_project', project_id) - return dict( - shares=dict(in_use=2, reserved=0), - snapshots=dict(in_use=4, reserved=0), - snapshot_gigabytes=dict(in_use=20, reserved=0), - gigabytes=dict(in_use=10, reserved=0)) - - self.mock_object(db, 'quota_get_all_by_project', fake_qgabp) - self.mock_object(db, 'quota_usage_get_all_by_project', fake_qugabp) - - self._stub_quota_class_get_all_by_name() - - def test_get_project_quotas(self): - self._stub_get_by_project() - result = self.driver.get_project_quotas( - FakeContext('test_project', 'test_class'), - quota.QUOTAS._resources, 'test_project') - - self.assertEqual(['quota_get_all_by_project', - 'quota_usage_get_all_by_project', - 'quota_class_get_all_by_name', ], self.calls) - self.assertEqual(self.expected_all_context, result) - - def test_get_project_quotas_with_remains(self): - self._stub_get_by_project() - result = self.driver.get_project_quotas( - FakeContext('test_project', 'test_class'), - quota.QUOTAS._resources, 'test_project', remains=True) - for result_key in result: - self.assertIn("remains", result[result_key]) - - def test_get_user_quotas_alt_context_no_class(self): - self._stub_get_by_project_and_user() - result = self.driver.get_user_quotas( - FakeContext('other_project', None), - quota.QUOTAS._resources, 'test_project', 'fake_user') - - self.assertEqual([ - 'quota_get_all_by_project_and_user', - 'quota_get_all_by_project', - 'quota_usage_get_all_by_project_and_user', ], self.calls) - self.assertEqual(self.expected_all_context, result) - - def test_get_project_quotas_alt_context_no_class(self): - self._stub_get_by_project() - result = self.driver.get_project_quotas( - FakeContext('other_project', None), - quota.QUOTAS._resources, 'test_project') - - self.assertEqual(['quota_get_all_by_project', - 'quota_usage_get_all_by_project', ], self.calls) - self.assertEqual(self.expected_all_context, result) - - def test_get_user_quotas_alt_context_with_class(self): - self._stub_get_by_project_and_user() - result = self.driver.get_user_quotas( - FakeContext('other_project', 'other_class'), - quota.QUOTAS._resources, 'test_project', 'fake_user', - quota_class='test_class') - - self.assertEqual([ - 'quota_get_all_by_project_and_user', - 'quota_get_all_by_project', - 'quota_usage_get_all_by_project_and_user', - 'quota_class_get_all_by_name', ], self.calls) - self.assertEqual(self.expected_all_context, result) - - def test_get_project_quotas_alt_context_with_class(self): - self._stub_get_by_project() - result = self.driver.get_project_quotas( - FakeContext('other_project', 'other_class'), - quota.QUOTAS._resources, 'test_project', quota_class='test_class') - - self.assertEqual(['quota_get_all_by_project', - 'quota_usage_get_all_by_project', - 'quota_class_get_all_by_name', ], self.calls) - self.assertEqual(self.expected_all_context, result) - - def test_get_user_quotas_no_defaults(self): - self._stub_get_by_project_and_user() - result = self.driver.get_user_quotas( - FakeContext('test_project', 'test_class'), - quota.QUOTAS._resources, 'test_project', 'fake_user', - defaults=False) - - self.assertEqual([ - 'quota_get_all_by_project_and_user', - 'quota_get_all_by_project', - 'quota_usage_get_all_by_project_and_user', - 'quota_class_get_all_by_name', ], self.calls) - expected = { - "shares": {"limit": 10, "in_use": 2, "reserved": 0, }, - "gigabytes": {"limit": 50, "in_use": 10, "reserved": 0, }, - "snapshot_gigabytes": {"limit": 50, "in_use": 20, "reserved": 0, }, - "snapshots": {"limit": 10, "in_use": 4, "reserved": 0, }, + @ddt.data( + {}, + {'quota_class': 'foo'}, + {'defaults': False}, + {'usages': False}, + {'remains': True}, + ) + def test_get_project_quotas(self, kwargs): + expected_kwargs = { + 'quota_class': None, + 'defaults': True, + 'usages': True, + 'remains': False, } - self.assertEqual(expected, result) - - def test_get_project_quotas_no_defaults(self): - self._stub_get_by_project() - result = self.driver.get_project_quotas( - FakeContext('test_project', 'test_class'), - quota.QUOTAS._resources, 'test_project', defaults=False) - - self.assertEqual(['quota_get_all_by_project', - 'quota_usage_get_all_by_project', - 'quota_class_get_all_by_name', ], self.calls) - expected = { - "shares": {"limit": 10, "in_use": 2, "reserved": 0, }, - "gigabytes": {"limit": 50, "in_use": 10, "reserved": 0, }, - "snapshot_gigabytes": {"limit": 50, "in_use": 20, "reserved": 0, }, - } - self.assertEqual(expected, result) - - def test_get_user_quotas_no_usages(self): - self._stub_get_by_project_and_user() - result = self.driver.get_user_quotas( - FakeContext('test_project', 'test_class'), - quota.QUOTAS._resources, 'test_project', 'fake_user', usages=False) - - self.assertEqual([ - 'quota_get_all_by_project_and_user', - 'quota_get_all_by_project', - 'quota_class_get_all_by_name', ], self.calls) - expected = { - "shares": {"limit": 10, }, - "gigabytes": {"limit": 50, }, - "snapshot_gigabytes": {"limit": 50, }, - "snapshots": {"limit": 10, }, - "share_networks": {"limit": 10, }, - } - self.assertEqual(expected, result, result) - - def test_get_project_quotas_no_usages(self): - self._stub_get_by_project() - result = self.driver.get_project_quotas( - FakeContext('test_project', 'test_class'), - quota.QUOTAS._resources, 'test_project', usages=False) - - self.assertEqual(['quota_get_all_by_project', - 'quota_class_get_all_by_name', ], self.calls) - expected = { - "shares": {"limit": 10, }, - "gigabytes": {"limit": 50, }, - "snapshot_gigabytes": {"limit": 50, }, - "snapshots": {"limit": 10, }, - "share_networks": {"limit": 10, }, - } - self.assertEqual(expected, result) - - def _stub_get_settable_quotas(self): - def fake_get_project_quotas(context, resources, project_id, - quota_class=None, defaults=True, - usages=True, remains=False): - self.calls.append('get_project_quotas') - result = {} - for k, v in resources.items(): - remains = v.default - in_use = 0 - result[k] = {'limit': v.default, 'in_use': in_use, - 'reserved': 0, 'remains': remains} - return result - - def fake_get_user_quotas(context, resources, project_id, user_id, - quota_class=None, defaults=True, - usages=True): - self.calls.append('get_user_quotas') - result = {} - for k, v in resources.items(): - in_use = 0 - result[k] = {'limit': v.default, - 'in_use': in_use, 'reserved': 0} - return result - - def fake_qgabpau(context, project_id, user_id): - self.calls.append('quota_get_all_by_project_and_user') - return {'shares': 2} - - self.mock_object(self.driver, 'get_project_quotas', - fake_get_project_quotas) - self.mock_object(self.driver, 'get_user_quotas', - fake_get_user_quotas) - self.mock_object(db, 'quota_get_all_by_project_and_user', - fake_qgabpau) - - def test_get_settable_quotas_with_user(self): - self._stub_get_settable_quotas() - result = self.driver.get_settable_quotas( - FakeContext('test_project', 'test_class'), - quota.QUOTAS._resources, 'test_project', user_id='test_user') - - self.assertEqual([ - 'get_project_quotas', - 'get_user_quotas', - 'quota_get_all_by_project_and_user', ], self.calls) - expected = { - "shares": {"minimum": 0, "maximum": 12, }, - "gigabytes": {"minimum": 0, "maximum": 1000, }, - "snapshot_gigabytes": {"minimum": 0, "maximum": 1000, }, - "snapshots": {"minimum": 0, "maximum": 10, }, - "share_networks": {"minimum": 0, "maximum": 10, }, - } - self.assertEqual(expected, result) - - def test_get_settable_quotas_without_user(self): - self._stub_get_settable_quotas() - result = self.driver.get_settable_quotas( - FakeContext('test_project', 'test_class'), - quota.QUOTAS._resources, 'test_project') - - self.assertEqual(['get_project_quotas', ], self.calls) - expected = { - "shares": {"minimum": 0, "maximum": -1, }, - "gigabytes": {"minimum": 0, "maximum": -1, }, - "snapshot_gigabytes": {"minimum": 0, "maximum": -1, }, - "snapshots": {"minimum": 0, "maximum": -1, }, - "share_networks": {"minimum": 0, "maximum": -1, }, - } - self.assertEqual(expected, result) - - def _stub_get_project_quotas(self): - def fake_get_project_quotas(context, resources, project_id, - quota_class=None, defaults=True, - usages=True): - self.calls.append('get_project_quotas') - return {k: dict(limit=v.default) - for k, v in resources.items()} - - self.mock_object(self.driver, 'get_project_quotas', - fake_get_project_quotas) - - def test_get_quotas_has_sync_unknown(self): - self._stub_get_project_quotas() - self.assertRaises(exception.QuotaResourceUnknown, - self.driver._get_quotas, - None, quota.QUOTAS._resources, - ['unknown'], True) - self.assertEqual([], self.calls) - - def test_get_quotas_no_sync_unknown(self): - self._stub_get_project_quotas() - self.assertRaises(exception.QuotaResourceUnknown, - self.driver._get_quotas, - None, quota.QUOTAS._resources, - ['unknown'], False) - self.assertEqual([], self.calls) - - def test_get_quotas_has_sync_no_sync_resource(self): - self._stub_get_project_quotas() - self.assertRaises(exception.QuotaResourceUnknown, - self.driver._get_quotas, - None, quota.QUOTAS._resources, - ['metadata_items'], True) - self.assertEqual([], self.calls) - - def test_get_quotas_no_sync_has_sync_resource(self): - self._stub_get_project_quotas() - self.assertRaises(exception.QuotaResourceUnknown, - self.driver._get_quotas, - None, quota.QUOTAS._resources, - ['shares'], False) - self.assertEqual([], self.calls) - - def test_get_quotas_has_sync(self): - self._stub_get_project_quotas() - result = self.driver._get_quotas(FakeContext('test_project', - 'test_class'), - quota.QUOTAS._resources, - ['shares', 'gigabytes'], - True) - - self.assertEqual(['get_project_quotas'], self.calls) - self.assertEqual(dict(shares=10, gigabytes=1000, ), result) - - def _stub_quota_reserve(self): - def fake_quota_reserve(context, resources, quotas, user_quotas, - deltas, expire, until_refresh, max_age, - project_id=None, user_id=None): - self.calls.append(('quota_reserve', expire, until_refresh, - max_age)) - return ['resv-1', 'resv-2', 'resv-3'] - self.mock_object(db, 'quota_reserve', fake_quota_reserve) - - def test_reserve_bad_expire(self): - self._stub_get_project_quotas() - self._stub_quota_reserve() - self.assertRaises(exception.InvalidReservationExpiration, - self.driver.reserve, - FakeContext('test_project', 'test_class'), - quota.QUOTAS._resources, - dict(shares=2), expire='invalid') - self.assertEqual([], self.calls) - - def test_reserve_default_expire(self): - self._stub_get_project_quotas() - self._stub_quota_reserve() - result = self.driver.reserve(FakeContext('test_project', 'test_class'), - quota.QUOTAS._resources, - dict(shares=2)) - - expire = timeutils.utcnow() + datetime.timedelta(seconds=86400) - self.assertEqual(['get_project_quotas', - ('quota_reserve', expire, 0, 0), ], self.calls) - self.assertEqual(['resv-1', 'resv-2', 'resv-3'], result) - - def test_reserve_int_expire(self): - self._stub_get_project_quotas() - self._stub_quota_reserve() - result = self.driver.reserve(FakeContext('test_project', 'test_class'), - quota.QUOTAS._resources, - dict(shares=2), expire=3600) - - expire = timeutils.utcnow() + datetime.timedelta(seconds=3600) - self.assertEqual(['get_project_quotas', - ('quota_reserve', expire, 0, 0), ], self.calls) - self.assertEqual(['resv-1', 'resv-2', 'resv-3'], result) - - def test_reserve_timedelta_expire(self): - self._stub_get_project_quotas() - self._stub_quota_reserve() - expire_delta = datetime.timedelta(seconds=60) - result = self.driver.reserve(FakeContext('test_project', 'test_class'), - quota.QUOTAS._resources, - dict(shares=2), expire=expire_delta) - - expire = timeutils.utcnow() + expire_delta - self.assertEqual(['get_project_quotas', - ('quota_reserve', expire, 0, 0), ], self.calls) - self.assertEqual(['resv-1', 'resv-2', 'resv-3'], result) - - def test_reserve_datetime_expire(self): - self._stub_get_project_quotas() - self._stub_quota_reserve() - expire = timeutils.utcnow() + datetime.timedelta(seconds=120) - result = self.driver.reserve(FakeContext('test_project', 'test_class'), - quota.QUOTAS._resources, - dict(shares=2), expire=expire) - - self.assertEqual(['get_project_quotas', - ('quota_reserve', expire, 0, 0), ], self.calls) - self.assertEqual(['resv-1', 'resv-2', 'resv-3'], result) - - def test_reserve_until_refresh(self): - self._stub_get_project_quotas() - self._stub_quota_reserve() - self.flags(until_refresh=500) - expire = timeutils.utcnow() + datetime.timedelta(seconds=120) - result = self.driver.reserve(FakeContext('test_project', 'test_class'), - quota.QUOTAS._resources, - dict(shares=2), expire=expire) - - self.assertEqual(['get_project_quotas', - ('quota_reserve', expire, 500, 0), ], self.calls) - self.assertEqual(['resv-1', 'resv-2', 'resv-3'], result) - - def test_reserve_max_age(self): - self._stub_get_project_quotas() - self._stub_quota_reserve() - self.flags(max_age=86400) - expire = timeutils.utcnow() + datetime.timedelta(seconds=120) - result = self.driver.reserve(FakeContext('test_project', 'test_class'), - quota.QUOTAS._resources, - dict(shares=2), expire=expire) - - self.assertEqual(['get_project_quotas', - ('quota_reserve', expire, 0, 86400), ], self.calls) - self.assertEqual(['resv-1', 'resv-2', 'resv-3'], result) - - def _stub_quota_delete_all_by_project(self): - def fake_quota_delete_all_by_project(context, project_id): - self.calls.append(('quota_destroy_all_by_project', project_id)) - return None - self.mock_object(sqa_api, 'quota_destroy_all_by_project', - fake_quota_delete_all_by_project) - - def test_delete_by_project(self): - self._stub_quota_delete_all_by_project() - self.driver.destroy_all_by_project(FakeContext('test_project', - 'test_class'), - 'test_project') - self.assertEqual([('quota_destroy_all_by_project', - ('test_project')), ], self.calls) - - -class FakeSession(object): - def begin(self): - return self - - def add(self, instance): - pass - - def __enter__(self): - return self - - def __exit__(self, exc_type, exc_value, exc_traceback): - return False - - -class FakeUsage(sqa_models.QuotaUsage): - def save(self, *args, **kwargs): - pass - - -class QuotaReserveSqlAlchemyTestCase(test.TestCase): - # manila.db.sqlalchemy.api.quota_reserve is so complex it needs its - # own test case, and since it's a quota manipulator, this is the - # best place to put it... - - def setUp(self): - super(QuotaReserveSqlAlchemyTestCase, self).setUp() - - self.sync_called = set() - - def make_sync(res_name): - def sync(context, project_id, user_id, session): - self.sync_called.add(res_name) - if res_name in self.usages: - if self.usages[res_name].in_use < 0: - return {res_name: 2} - else: - return {res_name: self.usages[res_name].in_use - 1} - return {res_name: 0} - return sync - - self.resources = {} - for res_name in ('shares', 'gigabytes'): - method_name = '_sync_%s' % res_name - sqa_api.QUOTA_SYNC_FUNCTIONS[method_name] = make_sync(res_name) - res = quota.ReservableResource(res_name, '_sync_%s' % res_name) - self.resources[res_name] = res - - self.expire = timeutils.utcnow() + datetime.timedelta(seconds=3600) - - self.usages = {} - self.usages_created = {} - self.reservations_created = {} - - def fake_get_session(): - return FakeSession() - - def fake_get_project_quota_usages(context, session, project_id): - return self.usages.copy() - - def fake_get_user_quota_usages(context, session, project_id, user_id): - return self.usages.copy() - - def fake_quota_usage_create(context, project_id, user_id, resource, - in_use, reserved, until_refresh, - session=None, save=True): - quota_usage_ref = self._make_quota_usage( - project_id, user_id, resource, in_use, reserved, until_refresh, - timeutils.utcnow(), timeutils.utcnow()) - - self.usages_created[resource] = quota_usage_ref - - return quota_usage_ref - - def fake_reservation_create(context, uuid, usage_id, project_id, - user_id, resource, delta, expire, - session=None): - reservation_ref = self._make_reservation( - uuid, usage_id, project_id, user_id, resource, delta, expire, - timeutils.utcnow(), timeutils.utcnow()) - - self.reservations_created[resource] = reservation_ref - - return reservation_ref - - self.mock_object(sqa_api, 'get_session', fake_get_session) - self.mock_object(sqa_api, '_get_project_quota_usages', - fake_get_project_quota_usages) - self.mock_object(sqa_api, '_get_user_quota_usages', - fake_get_user_quota_usages) - self.mock_object(sqa_api, '_quota_usage_create', - fake_quota_usage_create) - self.mock_object(sqa_api, '_reservation_create', - fake_reservation_create) - - self.patcher = mock.patch.object(timeutils, 'utcnow') - self.mock_utcnow = self.patcher.start() - self.mock_utcnow.return_value = datetime.datetime.utcnow() - - def tearDown(self): - self.patcher.stop() - super(QuotaReserveSqlAlchemyTestCase, self).tearDown() - - def _make_quota_usage(self, project_id, user_id, resource, in_use, - reserved, until_refresh, created_at, updated_at): - quota_usage_ref = FakeUsage() - quota_usage_ref.id = len(self.usages) + len(self.usages_created) - quota_usage_ref.project_id = project_id - quota_usage_ref.resource = resource - quota_usage_ref.in_use = in_use - quota_usage_ref.reserved = reserved - quota_usage_ref.until_refresh = until_refresh - quota_usage_ref.created_at = created_at - quota_usage_ref.updated_at = updated_at - quota_usage_ref.deleted_at = None - quota_usage_ref.deleted = False - - return quota_usage_ref - - def init_usage(self, project_id, user_id, resource, in_use, reserved, - until_refresh=None, created_at=None, updated_at=None): - if created_at is None: - created_at = timeutils.utcnow() - if updated_at is None: - updated_at = timeutils.utcnow() - - quota_usage_ref = self._make_quota_usage(project_id, user_id, - resource, in_use, - reserved, until_refresh, - created_at, updated_at) - - self.usages[resource] = quota_usage_ref - - def compare_usage(self, usage_dict, expected): - for usage in expected: - resource = usage['resource'] - for key, value in usage.items(): - actual = getattr(usage_dict[resource], key) - self.assertEqual(value, actual, - "%s != %s on usage for resource %s" % - (value, actual, resource)) - - def _make_reservation(self, uuid, usage_id, project_id, user_id, resource, - delta, expire, created_at, updated_at): - reservation_ref = sqa_models.Reservation() - reservation_ref.id = len(self.reservations_created) - reservation_ref.uuid = uuid - reservation_ref.usage_id = usage_id - reservation_ref.project_id = project_id - reservation_ref.resource = resource - reservation_ref.delta = delta - reservation_ref.expire = expire - reservation_ref.created_at = created_at - reservation_ref.updated_at = updated_at - reservation_ref.deleted_at = None - reservation_ref.deleted = False - - return reservation_ref - - def compare_reservation(self, reservations, expected): - reservations = set(reservations) - for resv in expected: - resource = resv['resource'] - resv_obj = self.reservations_created[resource] - - self.assertIn(resv_obj.uuid, reservations) - reservations.discard(resv_obj.uuid) - - for key, value in resv.items(): - actual = getattr(resv_obj, key) - self.assertEqual(value, actual, - "%s != %s on reservation for resource %s" % - (value, actual, resource)) - - self.assertEqual(0, len(reservations)) - - def test_quota_reserve_create_usages(self): - context = FakeContext('test_project', 'test_class') - quotas = dict(shares=5, - gigabytes=10 * 1024, ) - deltas = dict(shares=2, - gigabytes=2 * 1024, ) - result = sqa_api.quota_reserve(context, self.resources, quotas, - quotas, deltas, self.expire, 0, 0) - - self.assertEqual(set(['shares', 'gigabytes']), self.sync_called) - self.compare_usage(self.usages_created, - [dict(resource='shares', - project_id='test_project', - in_use=0, - reserved=2, - until_refresh=None), - dict(resource='gigabytes', - project_id='test_project', - in_use=0, - reserved=2 * 1024, - until_refresh=None), ]) - self.compare_reservation( - result, - [dict(resource='shares', - usage_id=self.usages_created['shares'], - project_id='test_project', - delta=2), - dict(resource='gigabytes', - usage_id=self.usages_created['gigabytes'], - delta=2 * 1024), ]) - - def test_quota_reserve_negative_in_use(self): - self.init_usage('test_project', 'test_user', 'shares', -1, 0, - until_refresh=1) - self.init_usage('test_project', 'test_user', 'gigabytes', -1, 0, - until_refresh=1) - context = FakeContext('test_project', 'test_class') - quotas = dict(shares=5, - gigabytes=10 * 1024, ) - deltas = dict(shares=2, - gigabytes=2 * 1024, ) - result = sqa_api.quota_reserve(context, self.resources, quotas, - quotas, deltas, self.expire, 5, 0) - - self.assertEqual(set(['shares', 'gigabytes']), self.sync_called) - self.compare_usage(self.usages, [dict(resource='shares', - project_id='test_project', - in_use=2, - reserved=2, - until_refresh=5), - dict(resource='gigabytes', - project_id='test_project', - in_use=2, - reserved=2 * 1024, - until_refresh=5), ]) - self.assertEqual({}, self.usages_created) - self.compare_reservation(result, - [dict(resource='shares', - usage_id=self.usages['shares'], - project_id='test_project', - delta=2), - dict(resource='gigabytes', - usage_id=self.usages['gigabytes'], - delta=2 * 1024), ]) - - def test_quota_reserve_until_refresh(self): - self.init_usage('test_project', 'test_user', 'shares', 3, 0, - until_refresh=1) - self.init_usage('test_project', 'test_user', 'gigabytes', 3, 0, - until_refresh=1) - context = FakeContext('test_project', 'test_class') - quotas = dict(shares=5, gigabytes=10 * 1024, ) - deltas = dict(shares=2, gigabytes=2 * 1024, ) - result = sqa_api.quota_reserve(context, self.resources, quotas, - quotas, deltas, self.expire, 5, 0) - - self.assertEqual(set(['shares', 'gigabytes']), self.sync_called) - self.compare_usage(self.usages, [dict(resource='shares', - project_id='test_project', - in_use=2, - reserved=2, - until_refresh=5), - dict(resource='gigabytes', - project_id='test_project', - in_use=2, - reserved=2 * 1024, - until_refresh=5), ]) - self.assertEqual({}, self.usages_created) - self.compare_reservation(result, - [dict(resource='shares', - usage_id=self.usages['shares'], - project_id='test_project', - delta=2), - dict(resource='gigabytes', - usage_id=self.usages['gigabytes'], - delta=2 * 1024), ]) - - def test_quota_reserve_max_age(self): - max_age = 3600 - record_created = (timeutils.utcnow() - - datetime.timedelta(seconds=max_age)) - self.init_usage('test_project', 'test_user', 'shares', 3, 0, - created_at=record_created, updated_at=record_created) - self.init_usage('test_project', 'test_user', 'gigabytes', 3, 0, - created_at=record_created, updated_at=record_created) - context = FakeContext('test_project', 'test_class') - quotas = dict(shares=5, gigabytes=10 * 1024, ) - deltas = dict(shares=2, gigabytes=2 * 1024, ) - result = sqa_api.quota_reserve(context, self.resources, quotas, - quotas, deltas, self.expire, 0, - max_age) - - self.assertEqual(set(['shares', 'gigabytes']), self.sync_called) - self.compare_usage(self.usages, [dict(resource='shares', - project_id='test_project', - in_use=2, - reserved=2, - until_refresh=None), - dict(resource='gigabytes', - project_id='test_project', - in_use=2, - reserved=2 * 1024, - until_refresh=None), ]) - self.assertEqual({}, self.usages_created) - self.compare_reservation(result, - [dict(resource='shares', - usage_id=self.usages['shares'], - project_id='test_project', - delta=2), - dict(resource='gigabytes', - usage_id=self.usages['gigabytes'], - delta=2 * 1024), ]) - - def test_quota_reserve_no_refresh(self): - self.init_usage('test_project', 'test_user', 'shares', 3, 0) - self.init_usage('test_project', 'test_user', 'gigabytes', 3, 0) - context = FakeContext('test_project', 'test_class') - quotas = dict(shares=5, gigabytes=10 * 1024, ) - deltas = dict(shares=2, gigabytes=2 * 1024, ) - result = sqa_api.quota_reserve(context, self.resources, quotas, - quotas, deltas, self.expire, 0, 0) - - self.assertEqual(set([]), self.sync_called) - self.compare_usage(self.usages, [dict(resource='shares', - project_id='test_project', - in_use=3, - reserved=2, - until_refresh=None), - dict(resource='gigabytes', - project_id='test_project', - in_use=3, - reserved=2 * 1024, - until_refresh=None), ]) - self.assertEqual({}, self.usages_created) - self.compare_reservation(result, - [dict(resource='shares', - usage_id=self.usages['shares'], - project_id='test_project', - delta=2), - dict(resource='gigabytes', - usage_id=self.usages['gigabytes'], - delta=2 * 1024), ]) - - def test_quota_reserve_unders(self): - self.init_usage('test_project', 'test_user', 'shares', 1, 0) - self.init_usage('test_project', 'test_user', 'gigabytes', 1 * 1024, 0) - context = FakeContext('test_project', 'test_class') - quotas = dict(shares=5, gigabytes=10 * 1024, ) - deltas = dict(shares=-2, gigabytes=-2 * 1024, ) - result = sqa_api.quota_reserve(context, self.resources, quotas, - quotas, deltas, self.expire, 0, 0) - - self.assertEqual(set([]), self.sync_called) - self.compare_usage(self.usages, [dict(resource='shares', - project_id='test_project', - in_use=1, - reserved=0, - until_refresh=None), - dict(resource='gigabytes', - project_id='test_project', - in_use=1 * 1024, - reserved=0, - until_refresh=None), ]) - self.assertEqual({}, self.usages_created) - self.compare_reservation(result, - [dict(resource='shares', - usage_id=self.usages['shares'], - project_id='test_project', - delta=-2), - dict(resource='gigabytes', - usage_id=self.usages['gigabytes'], - delta=-2 * 1024), ]) - - def test_quota_reserve_overs(self): - self.init_usage('test_project', 'test_user', 'shares', 4, 0) - self.init_usage('test_project', 'test_user', 'gigabytes', 10 * 1024, - 0) - context = FakeContext('test_project', 'test_class') - quotas = dict(shares=5, gigabytes=10 * 1024, ) - deltas = dict(shares=2, gigabytes=2 * 1024, ) - self.assertRaises(exception.OverQuota, - sqa_api.quota_reserve, - context, self.resources, quotas, quotas, - deltas, self.expire, 0, 0) - - self.assertEqual(set([]), self.sync_called) - self.compare_usage(self.usages, [dict(resource='shares', - project_id='test_project', - in_use=4, - reserved=0, - until_refresh=None), - dict(resource='gigabytes', - project_id='test_project', - in_use=10 * 1024, - reserved=0, - until_refresh=None), ]) - self.assertEqual({}, self.usages_created) - self.assertEqual({}, self.reservations_created) - - def test_quota_reserve_reduction(self): - self.init_usage('test_project', 'test_user', 'shares', 10, 0) - self.init_usage('test_project', 'test_user', 'gigabytes', 20 * 1024, - 0) - context = FakeContext('test_project', 'test_class') - quotas = dict(shares=5, gigabytes=10 * 1024, ) - deltas = dict(shares=-2, gigabytes=-2 * 1024, ) - result = sqa_api.quota_reserve(context, self.resources, quotas, - quotas, deltas, self.expire, 0, 0) - - self.assertEqual(set([]), self.sync_called) - self.compare_usage(self.usages, [dict(resource='shares', - project_id='test_project', - in_use=10, - reserved=0, - until_refresh=None), - dict(resource='gigabytes', - project_id='test_project', - in_use=20 * 1024, - reserved=0, - until_refresh=None), ]) - self.assertEqual({}, self.usages_created) - self.compare_reservation(result, - [dict(resource='shares', - usage_id=self.usages['shares'], - project_id='test_project', - delta=-2), - dict(resource='gigabytes', - usage_id=self.usages['gigabytes'], - project_id='test_project', - delta=-2 * 1024), ]) + expected_kwargs.update(kwargs) + + result = self.engine.get_project_quotas( + self.ctxt, self.project_id, **kwargs) + + self.assertEqual(result, self.driver.get_project_quotas.return_value) + self.driver.get_project_quotas.assert_called_once_with( + self.ctxt, self.engine._resources, + self.project_id, **expected_kwargs) + + @ddt.data( + {}, + {'user_id': 'fake_user_id'}, + {'share_type_id': 'fake_share_type_id'}, + ) + def test_get_settable_quotas(self, kwargs): + expected_kwargs = {'user_id': None, 'share_type_id': None} + expected_kwargs.update(kwargs) + + result = self.engine.get_settable_quotas( + self.ctxt, self.project_id, **kwargs) + + self.assertEqual(result, self.driver.get_settable_quotas.return_value) + self.driver.get_settable_quotas.assert_called_once_with( + self.ctxt, self.engine._resources, + self.project_id, **expected_kwargs) + + def test_count(self): + mock_count = mock.Mock() + resource = quota.CountableResource('FakeCountableResource', mock_count) + self.engine.register_resource(resource) + + result = self.engine.count(self.ctxt, resource.name) + + self.assertEqual(mock_count.return_value, result) + + def test_count_unknown_resource(self): + self.assertRaises( + exception.QuotaResourceUnknown, + self.engine.count, + self.ctxt, 'nonexistent_resource', 'foo_arg', foo='kwarg') + + def test_reserve(self): + result = self.engine.reserve( + self.ctxt, 'fake_expire', self.project_id, self.user_id, + self.share_type_id, delta1=1, delta2=2) + + self.assertEqual(self.driver.reserve.return_value, result) + self.driver.reserve.assert_called_once_with( + self.ctxt, self.engine._resources, {'delta1': 1, 'delta2': 2}, + expire='fake_expire', project_id=self.project_id, + user_id=self.user_id, share_type_id=self.share_type_id) + + @ddt.data(Exception('FakeException'), [None]) + def test_commit(self, side_effect): + fake_reservations = ['foo', 'bar'] + self.driver.commit.side_effect = side_effect + self.mock_object(quota.LOG, 'exception') + + result = self.engine.commit( + self.ctxt, fake_reservations, 'fake_project_id', + 'fake_user_id', 'fake_share_type_id') + + self.assertIsNone(result) + self.driver.commit.assert_called_once_with( + self.ctxt, fake_reservations, project_id='fake_project_id', + user_id='fake_user_id', share_type_id='fake_share_type_id') + if side_effect == [None]: + self.assertEqual(0, quota.LOG.exception.call_count) + else: + quota.LOG.exception.assert_called_once_with( + mock.ANY, fake_reservations) + + @ddt.data(Exception('FakeException'), [None]) + def test_rollback(self, side_effect): + fake_reservations = ['foo', 'bar'] + self.driver.rollback.side_effect = side_effect + self.mock_object(quota.LOG, 'exception') + + result = self.engine.rollback( + self.ctxt, fake_reservations, 'fake_project_id', + 'fake_user_id', 'fake_share_type_id') + + self.assertIsNone(result) + self.driver.rollback.assert_called_once_with( + self.ctxt, fake_reservations, project_id='fake_project_id', + user_id='fake_user_id', share_type_id='fake_share_type_id') + if side_effect == [None]: + self.assertEqual(0, quota.LOG.exception.call_count) + else: + quota.LOG.exception.assert_called_once_with( + mock.ANY, fake_reservations) + + def test_usage_reset(self): + result = self.engine.usage_reset(self.ctxt, 'fake_resources') + + self.assertIsNone(result) + self.driver.usage_reset.assert_called_once_with( + self.ctxt, 'fake_resources') + + def test_destroy_all_by_project_and_user(self): + result = self.engine.destroy_all_by_project_and_user( + self.ctxt, 'fake_project_id', 'fake_user_id') + + self.assertIsNone(result) + self.driver.destroy_all_by_project_and_user.assert_called_once_with( + self.ctxt, 'fake_project_id', 'fake_user_id') + + def test_destroy_all_by_project_and_share_type(self): + result = self.engine.destroy_all_by_project_and_share_type( + self.ctxt, 'fake_project_id', 'fake_st_id') + + self.assertIsNone(result) + mock_destroy_all_by_project_and_share_type = ( + self.driver.destroy_all_by_project_and_share_type) + mock_destroy_all_by_project_and_share_type.assert_called_once_with( + self.ctxt, 'fake_project_id', 'fake_st_id') + + def test_destroy_all_by_project(self): + result = self.engine.destroy_all_by_project( + self.ctxt, 'fake_project_id') + + self.assertIsNone(result) + self.driver.destroy_all_by_project.assert_called_once_with( + self.ctxt, 'fake_project_id') + + def test_expire(self): + result = self.engine.expire(self.ctxt) + + self.assertIsNone(result) + self.driver.expire.assert_called_once_with(self.ctxt) + + def test_resources(self): + self.engine.register_resources(self.resources) + self.assertEqual(['bar', 'foo'], self.engine.resources) + + def test_current_common_resources(self): + self.assertEqual( + ['gigabytes', 'share_networks', 'shares', + 'snapshot_gigabytes', 'snapshots'], + quota.QUOTAS.resources) diff --git a/manila_tempest_tests/config.py b/manila_tempest_tests/config.py index a2332be9d6..8b27700d5d 100644 --- a/manila_tempest_tests/config.py +++ b/manila_tempest_tests/config.py @@ -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", diff --git a/manila_tempest_tests/services/share/v2/json/shares_client.py b/manila_tempest_tests/services/share/v2/json/shares_client.py index 510b2f742a..3631c68b41 100644 --- a/manila_tempest_tests/services/share/v2/json/shares_client.py +++ b/manila_tempest_tests/services/share/v2/json/shares_client.py @@ -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: diff --git a/manila_tempest_tests/tests/api/admin/test_quotas.py b/manila_tempest_tests/tests/api/admin/test_quotas.py index fec32f3d94..741a2bfb47 100644 --- a/manila_tempest_tests/tests/api/admin/test_quotas.py +++ b/manila_tempest_tests/tests/api/admin/test_quotas.py @@ -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']) diff --git a/manila_tempest_tests/tests/api/admin/test_quotas_negative.py b/manila_tempest_tests/tests/api/admin/test_quotas_negative.py index b8557e14ac..fe4f562494 100644 --- a/manila_tempest_tests/tests/api/admin/test_quotas_negative.py +++ b/manila_tempest_tests/tests/api/admin/test_quotas_negative.py @@ -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) diff --git a/releasenotes/notes/add-share-type-quotas-33a6b36c0f4c88b1.yaml b/releasenotes/notes/add-share-type-quotas-33a6b36c0f4c88b1.yaml new file mode 100644 index 0000000000..304c9e125b --- /dev/null +++ b/releasenotes/notes/add-share-type-quotas-33a6b36c0f4c88b1.yaml @@ -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.