Coverage for manila/api/v2/quota_sets.py: 87%
229 statements
« prev ^ index » next coverage.py v7.11.0, created at 2026-02-18 22:19 +0000
« prev ^ index » next coverage.py v7.11.0, created at 2026-02-18 22:19 +0000
1# Copyright 2011 OpenStack LLC.
2# Copyright (c) 2015 Mirantis inc.
3# All Rights Reserved.
4#
5# Licensed under the Apache License, Version 2.0 (the "License"); you may
6# not use this file except in compliance with the License. You may obtain
7# a copy of the License at
8#
9# http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14# License for the specific language governing permissions and limitations
15# under the License.
17from http import client as http_client
18from urllib import parse
20from oslo_log import log
21from oslo_utils import strutils
22import webob
24from manila.api.openstack import api_version_request as api_version
25from manila.api.openstack import wsgi
26from manila.api.views import quota_sets as quota_sets_views
27from manila import db
28from manila import exception
29from manila.i18n import _
30from manila import quota
32QUOTAS = quota.QUOTAS
33LOG = log.getLogger(__name__)
34NON_QUOTA_KEYS = ('tenant_id', 'id', 'force', 'share_type')
37class QuotaSetsMixin(object):
38 """The Quota Sets API controller common logic.
40 Mixin class that should be inherited by Quota Sets API controllers,
41 which are used for different API URLs and microversions.
42 """
44 resource_name = "quota_set"
45 _view_builder_class = quota_sets_views.ViewBuilder
47 @staticmethod
48 def _validate_quota_limit(limit, minimum, maximum, force_update):
49 # NOTE: -1 is a flag value for unlimited
50 if limit < -1:
51 msg = _("Quota limit must be -1 or greater.")
52 raise webob.exc.HTTPBadRequest(explanation=msg)
53 if ((limit < minimum and not force_update) and 53 ↛ 55line 53 didn't jump to line 55 because the condition on line 53 was never true
54 (maximum != -1 or (maximum == -1 and limit != -1))):
55 msg = _("Quota limit must be greater than %s.") % minimum
56 raise webob.exc.HTTPBadRequest(explanation=msg)
57 if maximum != -1 and limit > maximum and not force_update:
58 msg = _("Quota limit must be less than %s.") % maximum
59 raise webob.exc.HTTPBadRequest(explanation=msg)
61 @staticmethod
62 def _validate_user_id_and_share_type_args(user_id, share_type):
63 if user_id and share_type:
64 msg = _("'user_id' and 'share_type' values are mutually exclusive")
65 raise webob.exc.HTTPBadRequest(explanation=msg)
67 @staticmethod
68 def _get_share_type_id(context, share_type_name_or_id):
69 if share_type_name_or_id:
70 share_type = db.share_type_get_by_name_or_id(
71 context, share_type_name_or_id)
72 if share_type:
73 return share_type['id']
74 msg = _("Share type with name or id '%s' not found.") % (
75 share_type_name_or_id)
76 raise webob.exc.HTTPNotFound(explanation=msg)
78 @staticmethod
79 def _ensure_share_type_arg_is_absent(req):
80 params = parse.parse_qs(req.environ.get('QUERY_STRING', ''))
81 share_type = params.get('share_type', [None])[0]
82 if share_type:
83 msg = _("'share_type' key is not supported by this microversion. "
84 "Use 2.39 or greater microversion to be able "
85 "to use 'share_type' quotas.")
86 raise webob.exc.HTTPBadRequest(explanation=msg)
88 @staticmethod
89 def _ensure_specific_microversion_args_are_absent(body, keys,
90 microversion):
91 body = body.get('quota_set', body)
92 for key in keys:
93 if body.get(key):
94 msg = (_("'%(key)s' key is not supported by this "
95 "microversion. Use %(microversion)s or greater "
96 "microversion to be able to use '%(key)s' quotas.") %
97 {"key": key, "microversion": microversion})
98 raise webob.exc.HTTPBadRequest(explanation=msg)
100 def _get_quotas(self, context, project_id, user_id=None,
101 share_type_id=None, usages=False):
102 self._validate_user_id_and_share_type_args(user_id, share_type_id)
103 if user_id:
104 values = QUOTAS.get_user_quotas(
105 context, project_id, user_id, usages=usages)
106 elif share_type_id:
107 values = QUOTAS.get_share_type_quotas(
108 context, project_id, share_type_id, usages=usages)
109 else:
110 values = QUOTAS.get_project_quotas(
111 context, project_id, usages=usages)
112 if usages:
113 return values
114 return {k: v['limit'] for k, v in values.items()}
116 @wsgi.Controller.authorize("show")
117 def _show(self, req, id, detail=False):
118 context = req.environ['manila.context']
119 params = parse.parse_qs(req.environ.get('QUERY_STRING', ''))
120 user_id = params.get('user_id', [None])[0]
121 share_type = params.get('share_type', [None])[0]
122 try:
123 db.authorize_project_context(context, id)
124 # _get_quotas use 'usages' to indicate whether retrieve additional
125 # attributes, so pass detail to the argument.
126 share_type_id = self._get_share_type_id(context, share_type)
127 quotas = self._get_quotas(
128 context, id, user_id, share_type_id, usages=detail)
129 return self._view_builder.detail_list(
130 req, quotas, id, share_type_id)
131 except exception.NotAuthorized:
132 raise webob.exc.HTTPForbidden()
134 @wsgi.Controller.authorize('show')
135 def _defaults(self, req, id):
136 context = req.environ['manila.context']
137 return self._view_builder.detail_list(
138 req, QUOTAS.get_defaults(context), id)
140 @wsgi.Controller.authorize("update")
141 def _update(self, req, id, body):
142 body = body.get('quota_set', {})
143 if (body.get('gigabytes') is None and
144 body.get('snapshots') is None and
145 body.get('snapshot_gigabytes') is None and
146 body.get('shares') is None and
147 body.get('share_networks') is None and
148 body.get('share_groups') is None and
149 body.get('share_group_snapshots') is None and
150 body.get('share_replicas') is None and
151 body.get('replica_gigabytes') is None and
152 body.get('per_share_gigabytes') is None and
153 body.get('backups') is None and
154 body.get('backup_gigabytes') is None and
155 body.get('encryption_keys') is None):
156 msg = _("Must supply at least one quota field to update.")
157 raise webob.exc.HTTPBadRequest(explanation=msg)
159 context = req.environ['manila.context']
160 project_id = id
161 bad_keys = []
162 force_update = False
163 params = parse.parse_qs(req.environ.get('QUERY_STRING', ''))
164 user_id = params.get('user_id', [None])[0]
165 share_type = params.get('share_type', [None])[0]
166 self._validate_user_id_and_share_type_args(user_id, share_type)
167 share_type_id = self._get_share_type_id(context, share_type)
168 if share_type and body.get('share_groups', 168 ↛ 170line 168 didn't jump to line 170 because the condition on line 168 was never true
169 body.get('share_group_snapshots')):
170 msg = _("Share type quotas cannot constrain share groups and "
171 "share group snapshots.")
172 raise webob.exc.HTTPBadRequest(explanation=msg)
174 try:
175 settable_quotas = QUOTAS.get_settable_quotas(
176 context, project_id, user_id=user_id,
177 share_type_id=share_type_id)
178 except exception.NotAuthorized:
179 raise webob.exc.HTTPForbidden()
181 for key, value in body.items():
182 if key == 'share_networks' and share_type_id:
183 msg = _("'share_networks' quota cannot be set for share type. "
184 "It can be set only for project or user.")
185 raise webob.exc.HTTPBadRequest(explanation=msg)
186 elif (key not in QUOTAS and key not in NON_QUOTA_KEYS): 186 ↛ 187line 186 didn't jump to line 187 because the condition on line 186 was never true
187 bad_keys.append(key)
188 elif key == 'force':
189 force_update = strutils.bool_from_string(value)
190 elif key not in NON_QUOTA_KEYS and value:
191 try:
192 value = int(value)
193 except (ValueError, TypeError):
194 msg = _("Quota '%(value)s' for %(key)s should be "
195 "integer.") % {'value': value, 'key': key}
196 LOG.warning(msg)
197 raise webob.exc.HTTPBadRequest(explanation=msg)
199 LOG.debug("Force update quotas: %s.", force_update)
201 if len(bad_keys) > 0: 201 ↛ 202line 201 didn't jump to line 202 because the condition on line 201 was never true
202 msg = _("Bad key(s) %s in quota_set.") % ",".join(bad_keys)
203 raise webob.exc.HTTPBadRequest(explanation=msg)
205 try:
206 quotas = self._get_quotas(
207 context, id, user_id=user_id, share_type_id=share_type_id,
208 usages=True)
209 except exception.NotAuthorized:
210 raise webob.exc.HTTPForbidden()
212 for key, value in body.items():
213 if key in NON_QUOTA_KEYS or (not value and value != 0):
214 continue
215 # validate whether already used and reserved exceeds the new
216 # quota, this check will be ignored if admin want to force
217 # update
218 try:
219 value = int(value)
220 except (ValueError, TypeError):
221 msg = _("Quota '%(value)s' for %(key)s should be "
222 "integer.") % {'value': value, 'key': key}
223 LOG.warning(msg)
224 raise webob.exc.HTTPBadRequest(explanation=msg)
226 if force_update is False and value >= 0:
227 quota_value = quotas.get(key)
228 if quota_value and quota_value['limit'] >= 0: 228 ↛ 243line 228 didn't jump to line 243 because the condition on line 228 was always true
229 quota_used = (quota_value['in_use'] +
230 quota_value['reserved'])
231 LOG.debug("Quota %(key)s used: %(quota_used)s, "
232 "value: %(value)s.",
233 {'key': key, 'quota_used': quota_used,
234 'value': value})
235 if quota_used > value: 235 ↛ 236line 235 didn't jump to line 236 because the condition on line 235 was never true
236 msg = (_("Quota value %(value)s for %(key)s is "
237 "smaller than already used and reserved "
238 "%(quota_used)s.") %
239 {'value': value, 'key': key,
240 'quota_used': quota_used})
241 raise webob.exc.HTTPBadRequest(explanation=msg)
243 minimum = settable_quotas[key]['minimum']
244 maximum = settable_quotas[key]['maximum']
245 self._validate_quota_limit(value, minimum, maximum, force_update)
246 try:
247 db.quota_create(
248 context, project_id, key, value,
249 user_id=user_id, share_type_id=share_type_id)
250 except exception.QuotaExists:
251 db.quota_update(
252 context, project_id, key, value,
253 user_id=user_id, share_type_id=share_type_id)
254 except exception.AdminRequired:
255 raise webob.exc.HTTPForbidden()
256 return self._view_builder.detail_list(
257 req,
258 self._get_quotas(
259 context, id, user_id=user_id, share_type_id=share_type_id),
260 share_type=share_type_id,
261 )
263 @wsgi.Controller.authorize("delete")
264 def _delete(self, req, id):
265 context = req.environ['manila.context']
266 params = parse.parse_qs(req.environ.get('QUERY_STRING', ''))
267 user_id = params.get('user_id', [None])[0]
268 share_type = params.get('share_type', [None])[0]
269 self._validate_user_id_and_share_type_args(user_id, share_type)
270 try:
271 db.authorize_project_context(context, id)
272 if user_id:
273 QUOTAS.destroy_all_by_project_and_user(context, id, user_id)
274 elif share_type:
275 share_type_id = self._get_share_type_id(context, share_type)
276 QUOTAS.destroy_all_by_project_and_share_type(
277 context, id, share_type_id)
278 else:
279 QUOTAS.destroy_all_by_project(context, id)
280 return webob.Response(status_int=http_client.ACCEPTED)
281 except exception.NotAuthorized:
282 raise webob.exc.HTTPForbidden()
285class QuotaSetsControllerLegacy(QuotaSetsMixin, wsgi.Controller):
286 """Deprecated Quota Sets API controller.
288 Used from microversions 2.0 to 2.6. Registered under deprecated API URL
289 'os-quota-sets'.
290 """
292 @wsgi.Controller.api_version('1.0', '2.6')
293 def show(self, req, id):
294 self._ensure_share_type_arg_is_absent(req)
295 return self._show(req, id)
297 @wsgi.Controller.api_version('1.0', '2.6')
298 def defaults(self, req, id):
299 return self._defaults(req, id)
301 @wsgi.Controller.api_version('1.0', '2.6')
302 def update(self, req, id, body):
303 self._ensure_share_type_arg_is_absent(req)
304 self._ensure_specific_microversion_args_are_absent(
305 body, ['share_groups', 'share_group_snapshots'], "2.40")
306 self._ensure_specific_microversion_args_are_absent(
307 body, ['share_replicas', 'replica_gigabytes'], "2.53")
308 return self._update(req, id, body)
310 @wsgi.Controller.api_version('1.0', '2.6')
311 def delete(self, req, id):
312 self._ensure_share_type_arg_is_absent(req)
313 return self._delete(req, id)
316class QuotaSetsController(QuotaSetsMixin, wsgi.Controller):
317 """Quota Sets API controller.
319 Used from microversion 2.7. Registered under API URL 'quota-sets'.
320 """
322 @wsgi.Controller.api_version('2.7')
323 def show(self, req, id):
324 if req.api_version_request < api_version.APIVersionRequest("2.39"):
325 self._ensure_share_type_arg_is_absent(req)
326 return self._show(req, id)
328 @wsgi.Controller.api_version('2.25')
329 def detail(self, req, id):
330 if req.api_version_request < api_version.APIVersionRequest("2.39"):
331 self._ensure_share_type_arg_is_absent(req)
332 return self._show(req, id, True)
334 @wsgi.Controller.api_version('2.7')
335 def defaults(self, req, id):
336 return self._defaults(req, id)
338 @wsgi.Controller.api_version('2.7')
339 def update(self, req, id, body):
340 if req.api_version_request < api_version.APIVersionRequest("2.39"):
341 self._ensure_share_type_arg_is_absent(req)
342 elif req.api_version_request < api_version.APIVersionRequest("2.40"):
343 self._ensure_specific_microversion_args_are_absent(
344 body, ['share_groups', 'share_group_snapshots'], "2.40")
345 elif req.api_version_request < api_version.APIVersionRequest("2.53"): 345 ↛ 348line 345 didn't jump to line 348 because the condition on line 345 was always true
346 self._ensure_specific_microversion_args_are_absent(
347 body, ['share_replicas', 'replica_gigabytes'], "2.53")
348 elif req.api_version_request < api_version.APIVersionRequest("2.62"):
349 self._ensure_specific_microversion_args_are_absent(
350 body, ['per_share_gigabytes'], "2.62")
351 elif req.api_version_request < api_version.APIVersionRequest("2.80"):
352 self._ensure_specific_microversion_args_are_absent(
353 body, ['backups', 'backup_gigabytes'], "2.80")
354 elif req.api_version_request < api_version.APIVersionRequest("2.90"):
355 self._ensure_specific_microversion_args_are_absent(
356 body, ['encryption_keys'], "2.90")
357 return self._update(req, id, body)
359 @wsgi.Controller.api_version('2.7')
360 def delete(self, req, id):
361 if req.api_version_request < api_version.APIVersionRequest("2.39"):
362 self._ensure_share_type_arg_is_absent(req)
363 return self._delete(req, id)
366def create_resource_legacy():
367 return wsgi.Resource(QuotaSetsControllerLegacy())
370def create_resource():
371 return wsgi.Resource(QuotaSetsController())