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

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. 

16 

17from http import client as http_client 

18from urllib import parse 

19 

20from oslo_log import log 

21from oslo_utils import strutils 

22import webob 

23 

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 

31 

32QUOTAS = quota.QUOTAS 

33LOG = log.getLogger(__name__) 

34NON_QUOTA_KEYS = ('tenant_id', 'id', 'force', 'share_type') 

35 

36 

37class QuotaSetsMixin(object): 

38 """The Quota Sets API controller common logic. 

39 

40 Mixin class that should be inherited by Quota Sets API controllers, 

41 which are used for different API URLs and microversions. 

42 """ 

43 

44 resource_name = "quota_set" 

45 _view_builder_class = quota_sets_views.ViewBuilder 

46 

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) 

60 

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) 

66 

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) 

77 

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) 

87 

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) 

99 

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()} 

115 

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() 

133 

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) 

139 

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) 

158 

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) 

173 

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() 

180 

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) 

198 

199 LOG.debug("Force update quotas: %s.", force_update) 

200 

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) 

204 

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() 

211 

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) 

225 

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) 

242 

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 ) 

262 

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() 

283 

284 

285class QuotaSetsControllerLegacy(QuotaSetsMixin, wsgi.Controller): 

286 """Deprecated Quota Sets API controller. 

287 

288 Used from microversions 2.0 to 2.6. Registered under deprecated API URL 

289 'os-quota-sets'. 

290 """ 

291 

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) 

296 

297 @wsgi.Controller.api_version('1.0', '2.6') 

298 def defaults(self, req, id): 

299 return self._defaults(req, id) 

300 

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) 

309 

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) 

314 

315 

316class QuotaSetsController(QuotaSetsMixin, wsgi.Controller): 

317 """Quota Sets API controller. 

318 

319 Used from microversion 2.7. Registered under API URL 'quota-sets'. 

320 """ 

321 

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) 

327 

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) 

333 

334 @wsgi.Controller.api_version('2.7') 

335 def defaults(self, req, id): 

336 return self._defaults(req, id) 

337 

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) 

358 

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) 

364 

365 

366def create_resource_legacy(): 

367 return wsgi.Resource(QuotaSetsControllerLegacy()) 

368 

369 

370def create_resource(): 

371 return wsgi.Resource(QuotaSetsController())