Coverage for nova/quota.py: 92%

424 statements  

« prev     ^ index     » next       coverage.py v7.6.12, created at 2025-04-17 15:08 +0000

1# Copyright 2010 United States Government as represented by the 

2# Administrator of the National Aeronautics and Space Administration. 

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 

17"""Quotas for resources per project.""" 

18 

19import copy 

20 

21from oslo_log import log as logging 

22from oslo_utils import importutils 

23from sqlalchemy import sql 

24 

25import nova.conf 

26from nova import context as nova_context 

27from nova.db.api import api as api_db_api 

28from nova.db.api import models as api_models 

29from nova.db.main import api as main_db_api 

30from nova import exception 

31from nova.limit import local as local_limit 

32from nova.limit import placement as placement_limit 

33from nova import objects 

34from nova.scheduler.client import report 

35from nova import utils 

36 

37LOG = logging.getLogger(__name__) 

38CONF = nova.conf.CONF 

39# Lazy-loaded on first access. 

40# Avoid constructing the KSA adapter and provider tree on every access. 

41PLACEMENT_CLIENT = None 

42# If user_id and queued_for_delete are populated for a project, cache the 

43# result to avoid doing unnecessary EXISTS database queries. 

44UID_QFD_POPULATED_CACHE_BY_PROJECT = set() 

45# For the server group members check, we do not scope to a project, so if all 

46# user_id and queued_for_delete are populated for all projects, cache the 

47# result to avoid doing unnecessary EXISTS database queries. 

48UID_QFD_POPULATED_CACHE_ALL = False 

49 

50 

51class DbQuotaDriver(object): 

52 """Driver to perform necessary checks to enforce quotas and obtain 

53 quota information. The default driver utilizes the local 

54 database. 

55 """ 

56 UNLIMITED_VALUE = -1 

57 

58 def get_reserved(self): 

59 # Since we stopped reserving the DB, we just return 0 

60 return 0 

61 

62 def get_defaults(self, context, resources): 

63 """Given a list of resources, retrieve the default quotas. 

64 Use the class quotas named `_DEFAULT_QUOTA_NAME` as default quotas, 

65 if it exists. 

66 

67 :param context: The request context, for access checks. 

68 :param resources: A dictionary of the registered resources. 

69 """ 

70 

71 quotas = {} 

72 default_quotas = objects.Quotas.get_default_class(context) 

73 for resource in resources.values(): 

74 # resource.default returns the config options. So if there's not 

75 # an entry for the resource in the default class, it uses the 

76 # config option. 

77 quotas[resource.name] = default_quotas.get(resource.name, 

78 resource.default) 

79 

80 return quotas 

81 

82 def get_class_quotas(self, context, resources, quota_class): 

83 """Given a list of resources, retrieve the quotas for the given 

84 quota class. 

85 

86 :param context: The request context, for access checks. 

87 :param resources: A dictionary of the registered resources. 

88 :param quota_class: The name of the quota class to return 

89 quotas for. 

90 """ 

91 

92 quotas = {} 

93 class_quotas = objects.Quotas.get_all_class_by_name(context, 

94 quota_class) 

95 for resource in resources.values(): 

96 quotas[resource.name] = class_quotas.get(resource.name, 

97 resource.default) 

98 

99 return quotas 

100 

101 def _process_quotas(self, context, resources, project_id, quotas, 

102 quota_class=None, usages=None, 

103 remains=False): 

104 modified_quotas = {} 

105 # Get the quotas for the appropriate class. If the project ID 

106 # matches the one in the context, we use the quota_class from 

107 # the context, otherwise, we use the provided quota_class (if 

108 # any) 

109 if project_id == context.project_id: 

110 quota_class = context.quota_class 

111 if quota_class: 

112 class_quotas = objects.Quotas.get_all_class_by_name(context, 

113 quota_class) 

114 else: 

115 class_quotas = {} 

116 

117 default_quotas = self.get_defaults(context, resources) 

118 

119 for resource in resources.values(): 

120 limit = quotas.get(resource.name, class_quotas.get( 

121 resource.name, default_quotas[resource.name])) 

122 modified_quotas[resource.name] = dict(limit=limit) 

123 

124 # Include usages if desired. This is optional because one 

125 # internal consumer of this interface wants to access the 

126 # usages directly from inside a transaction. 

127 if usages: 

128 usage = usages.get(resource.name, {}) 

129 modified_quotas[resource.name].update( 

130 in_use=usage.get('in_use', 0), 

131 ) 

132 

133 # Initialize remains quotas with the default limits. 

134 if remains: 

135 modified_quotas[resource.name].update(remains=limit) 

136 

137 if remains: 

138 # Get all user quotas for a project and subtract their limits 

139 # from the class limits to get the remains. For example, if the 

140 # class/default is 20 and there are two users each with quota of 5, 

141 # then there is quota of 10 left to give out. 

142 all_quotas = objects.Quotas.get_all(context, project_id) 

143 for quota in all_quotas: 

144 if quota.resource in modified_quotas: 144 ↛ 143line 144 didn't jump to line 143 because the condition on line 144 was always true

145 modified_quotas[quota.resource]['remains'] -= \ 

146 quota.hard_limit 

147 

148 return modified_quotas 

149 

150 def _get_usages(self, context, resources, project_id, user_id=None): 

151 """Get usages of specified resources. 

152 

153 This function is called to get resource usages for validating quota 

154 limit creates or updates in the os-quota-sets API and for displaying 

155 resource usages in the os-used-limits API. This function is not used 

156 for checking resource usage against quota limits. 

157 

158 :param context: The request context for access checks 

159 :param resources: The dict of Resources for which to get usages 

160 :param project_id: The project_id for scoping the usage count 

161 :param user_id: Optional user_id for scoping the usage count 

162 :returns: A dict containing resources and their usage information, 

163 for example: 

164 {'project_id': 'project-uuid', 

165 'user_id': 'user-uuid', 

166 'instances': {'in_use': 5}} 

167 """ 

168 usages = {} 

169 for resource in resources.values(): 

170 # NOTE(melwitt): We should skip resources that are not countable, 

171 # such as AbsoluteResources. 

172 if not isinstance(resource, CountableResource): 

173 continue 

174 if resource.name in usages: 

175 # This is needed because for any of the resources: 

176 # ('instances', 'cores', 'ram'), they are counted at the same 

177 # time for efficiency (query the instances table once instead 

178 # of multiple times). So, a count of any one of them contains 

179 # counts for the others and we can avoid re-counting things. 

180 continue 

181 if resource.name in ('key_pairs', 'server_group_members'): 

182 # These per user resources are special cases whose usages 

183 # are not considered when validating limit create/update or 

184 # displaying used limits. They are always zero. 

185 usages[resource.name] = {'in_use': 0} 

186 else: 

187 if ( 187 ↛ 191line 187 didn't jump to line 191 because the condition on line 187 was never true

188 resource.name in 

189 main_db_api.quota_get_per_project_resources() 

190 ): 

191 count = resource.count_as_dict(context, project_id) 

192 key = 'project' 

193 else: 

194 # NOTE(melwitt): This assumes a specific signature for 

195 # count_as_dict(). Usages used to be records in the 

196 # database but now we are counting resources. The 

197 # count_as_dict() function signature needs to match this 

198 # call, else it should get a conditional in this function. 

199 count = resource.count_as_dict(context, project_id, 

200 user_id=user_id) 

201 key = 'user' if user_id else 'project' 

202 # Example count_as_dict() return value: 

203 # {'project': {'instances': 5}, 

204 # 'user': {'instances': 2}} 

205 counted_resources = count[key].keys() 

206 for res in counted_resources: 

207 count_value = count[key][res] 

208 usages[res] = {'in_use': count_value} 

209 return usages 

210 

211 def get_user_quotas(self, context, resources, project_id, user_id, 

212 quota_class=None, 

213 usages=True, project_quotas=None, 

214 user_quotas=None): 

215 """Given a list of resources, retrieve the quotas for the given 

216 user and project. 

217 

218 :param context: The request context, for access checks. 

219 :param resources: A dictionary of the registered resources. 

220 :param project_id: The ID of the project to return quotas for. 

221 :param user_id: The ID of the user to return quotas for. 

222 :param quota_class: If project_id != context.project_id, the 

223 quota class cannot be determined. This 

224 parameter allows it to be specified. It 

225 will be ignored if project_id == 

226 context.project_id. 

227 :param usages: If True, the current counts will also be returned. 

228 :param project_quotas: Quotas dictionary for the specified project. 

229 :param user_quotas: Quotas dictionary for the specified project 

230 and user. 

231 """ 

232 if user_quotas: 

233 user_quotas = user_quotas.copy() 

234 else: 

235 user_quotas = objects.Quotas.get_all_by_project_and_user( 

236 context, project_id, user_id) 

237 # Use the project quota for default user quota. 

238 proj_quotas = project_quotas or objects.Quotas.get_all_by_project( 

239 context, project_id) 

240 for key, value in proj_quotas.items(): 

241 if key not in user_quotas.keys(): 

242 user_quotas[key] = value 

243 user_usages = {} 

244 if usages: 

245 user_usages = self._get_usages(context, resources, project_id, 

246 user_id=user_id) 

247 return self._process_quotas(context, resources, project_id, 

248 user_quotas, quota_class, 

249 usages=user_usages) 

250 

251 def get_project_quotas(self, context, resources, project_id, 

252 quota_class=None, 

253 usages=True, remains=False, project_quotas=None): 

254 """Given a list of resources, retrieve the quotas for the given 

255 project. 

256 

257 :param context: The request context, for access checks. 

258 :param resources: A dictionary of the registered resources. 

259 :param project_id: The ID of the project to return quotas for. 

260 :param quota_class: If project_id != context.project_id, the 

261 quota class cannot be determined. This 

262 parameter allows it to be specified. It 

263 will be ignored if project_id == 

264 context.project_id. 

265 :param usages: If True, the current counts will also be returned. 

266 :param remains: If True, the current remains of the project will 

267 will be returned. 

268 :param project_quotas: Quotas dictionary for the specified project. 

269 """ 

270 project_quotas = project_quotas or objects.Quotas.get_all_by_project( 

271 context, project_id) 

272 project_usages = {} 

273 if usages: 

274 project_usages = self._get_usages(context, resources, project_id) 

275 return self._process_quotas(context, resources, project_id, 

276 project_quotas, quota_class, 

277 usages=project_usages, 

278 remains=remains) 

279 

280 def _is_unlimited_value(self, v): 

281 """A helper method to check for unlimited value. 

282 """ 

283 

284 return v <= self.UNLIMITED_VALUE 

285 

286 def _sum_quota_values(self, v1, v2): 

287 """A helper method that handles unlimited values when performing 

288 sum operation. 

289 """ 

290 

291 if self._is_unlimited_value(v1) or self._is_unlimited_value(v2): 

292 return self.UNLIMITED_VALUE 

293 return v1 + v2 

294 

295 def _sub_quota_values(self, v1, v2): 

296 """A helper method that handles unlimited values when performing 

297 subtraction operation. 

298 """ 

299 

300 if self._is_unlimited_value(v1) or self._is_unlimited_value(v2): 

301 return self.UNLIMITED_VALUE 

302 return v1 - v2 

303 

304 def get_settable_quotas(self, context, resources, project_id, 

305 user_id=None): 

306 """Given a list of resources, retrieve the range of settable quotas for 

307 the given user or project. 

308 

309 :param context: The request context, for access checks. 

310 :param resources: A dictionary of the registered resources. 

311 :param project_id: The ID of the project to return quotas for. 

312 :param user_id: The ID of the user to return quotas for. 

313 """ 

314 

315 settable_quotas = {} 

316 db_proj_quotas = objects.Quotas.get_all_by_project(context, project_id) 

317 project_quotas = self.get_project_quotas(context, resources, 

318 project_id, remains=True, 

319 project_quotas=db_proj_quotas) 

320 if user_id: 

321 setted_quotas = objects.Quotas.get_all_by_project_and_user( 

322 context, project_id, user_id) 

323 user_quotas = self.get_user_quotas(context, resources, 

324 project_id, user_id, 

325 project_quotas=db_proj_quotas, 

326 user_quotas=setted_quotas) 

327 for key, value in user_quotas.items(): 

328 # Maximum is the remaining quota for a project (class/default 

329 # minus the sum of all user quotas in the project), plus the 

330 # given user's quota. So if the class/default is 20 and there 

331 # are two users each with quota of 5, then there is quota of 

332 # 10 remaining. The given user currently has quota of 5, so 

333 # the maximum you could update their quota to would be 15. 

334 # Class/default 20 - currently used in project 10 + current 

335 # user 5 = 15. 

336 maximum = \ 

337 self._sum_quota_values(project_quotas[key]['remains'], 

338 setted_quotas.get(key, 0)) 

339 # This function is called for the quota_sets api and the 

340 # corresponding nova-manage command. The idea is when someone 

341 # attempts to update a quota, the value chosen must be at least 

342 # as much as the current usage and less than or equal to the 

343 # project limit less the sum of existing per user limits. 

344 minimum = value['in_use'] 

345 settable_quotas[key] = {'minimum': minimum, 'maximum': maximum} 

346 else: 

347 for key, value in project_quotas.items(): 

348 minimum = \ 

349 max(int(self._sub_quota_values(value['limit'], 

350 value['remains'])), 

351 int(value['in_use'])) 

352 settable_quotas[key] = {'minimum': minimum, 'maximum': -1} 

353 return settable_quotas 

354 

355 def _get_quotas(self, context, resources, keys, project_id=None, 

356 user_id=None, project_quotas=None): 

357 """A helper method which retrieves the quotas for the specific 

358 resources identified by keys, and which apply to the current 

359 context. 

360 

361 :param context: The request context, for access checks. 

362 :param resources: A dictionary of the registered resources. 

363 :param keys: A list of the desired quotas to retrieve. 

364 :param project_id: Specify the project_id if current context 

365 is admin and admin wants to impact on 

366 common user's tenant. 

367 :param user_id: Specify the user_id if current context 

368 is admin and admin wants to impact on 

369 common user. 

370 :param project_quotas: Quotas dictionary for the specified project. 

371 """ 

372 

373 # Filter resources 

374 desired = set(keys) 

375 sub_resources = {k: v for k, v in resources.items() if k in desired} 

376 

377 # Make sure we accounted for all of them... 

378 if len(keys) != len(sub_resources): 

379 unknown = desired - set(sub_resources.keys()) 

380 raise exception.QuotaResourceUnknown(unknown=sorted(unknown)) 

381 

382 if user_id: 

383 LOG.debug('Getting quotas for user %(user_id)s and project ' 

384 '%(project_id)s. Resources: %(keys)s', 

385 {'user_id': user_id, 'project_id': project_id, 

386 'keys': keys}) 

387 # Grab and return the quotas (without usages) 

388 quotas = self.get_user_quotas(context, sub_resources, 

389 project_id, user_id, 

390 context.quota_class, usages=False, 

391 project_quotas=project_quotas) 

392 else: 

393 LOG.debug('Getting quotas for project %(project_id)s. Resources: ' 

394 '%(keys)s', {'project_id': project_id, 'keys': keys}) 

395 # Grab and return the quotas (without usages) 

396 quotas = self.get_project_quotas(context, sub_resources, 

397 project_id, 

398 context.quota_class, 

399 usages=False, 

400 project_quotas=project_quotas) 

401 

402 return {k: v['limit'] for k, v in quotas.items()} 

403 

404 def limit_check(self, context, resources, values, project_id=None, 

405 user_id=None): 

406 """Check simple quota limits. 

407 

408 For limits--those quotas for which there is no usage 

409 synchronization function--this method checks that a set of 

410 proposed values are permitted by the limit restriction. 

411 

412 This method will raise a QuotaResourceUnknown exception if a 

413 given resource is unknown or if it is not a simple limit 

414 resource. 

415 

416 If any of the proposed values is over the defined quota, an 

417 OverQuota exception will be raised with the sorted list of the 

418 resources which are too high. Otherwise, the method returns 

419 nothing. 

420 

421 :param context: The request context, for access checks. 

422 :param resources: A dictionary of the registered resources. 

423 :param values: A dictionary of the values to check against the 

424 quota. 

425 :param project_id: Specify the project_id if current context 

426 is admin and admin wants to impact on 

427 common user's tenant. 

428 :param user_id: Specify the user_id if current context 

429 is admin and admin wants to impact on 

430 common user. 

431 """ 

432 _valid_method_call_check_resources(values, 'check', resources) 

433 

434 # Ensure no value is less than zero 

435 unders = [key for key, val in values.items() if val < 0] 

436 if unders: 

437 raise exception.InvalidQuotaValue(unders=sorted(unders)) 

438 

439 # If project_id is None, then we use the project_id in context 

440 if project_id is None: 440 ↛ 443line 440 didn't jump to line 443 because the condition on line 440 was always true

441 project_id = context.project_id 

442 # If user id is None, then we use the user_id in context 

443 if user_id is None: 443 ↛ 447line 443 didn't jump to line 447 because the condition on line 443 was always true

444 user_id = context.user_id 

445 

446 # Get the applicable quotas 

447 project_quotas = objects.Quotas.get_all_by_project(context, project_id) 

448 quotas = self._get_quotas(context, resources, values.keys(), 

449 project_id=project_id, 

450 project_quotas=project_quotas) 

451 user_quotas = self._get_quotas(context, resources, values.keys(), 

452 project_id=project_id, 

453 user_id=user_id, 

454 project_quotas=project_quotas) 

455 

456 # Check the quotas and construct a list of the resources that 

457 # would be put over limit by the desired values 

458 overs = [key for key, val in values.items() 

459 if quotas[key] >= 0 and quotas[key] < val or 

460 (user_quotas[key] >= 0 and user_quotas[key] < val)] 

461 if overs: 

462 headroom = {} 

463 for key in overs: 

464 headroom[key] = min( 

465 val for val in (quotas.get(key), project_quotas.get(key)) 

466 if val is not None 

467 ) 

468 raise exception.OverQuota(overs=sorted(overs), quotas=quotas, 

469 usages={}, headroom=headroom) 

470 

471 def limit_check_project_and_user(self, context, resources, 

472 project_values=None, user_values=None, 

473 project_id=None, user_id=None): 

474 """Check values (usage + desired delta) against quota limits. 

475 

476 For limits--this method checks that a set of 

477 proposed values are permitted by the limit restriction. 

478 

479 This method will raise a QuotaResourceUnknown exception if a 

480 given resource is unknown or if it is not a simple limit 

481 resource. 

482 

483 If any of the proposed values is over the defined quota, an 

484 OverQuota exception will be raised with the sorted list of the 

485 resources which are too high. Otherwise, the method returns 

486 nothing. 

487 

488 :param context: The request context, for access checks 

489 :param resources: A dictionary of the registered resources 

490 :param project_values: Optional dict containing the resource values to 

491 check against project quota, 

492 e.g. {'instances': 1, 'cores': 2, 'memory_mb': 512} 

493 :param user_values: Optional dict containing the resource values to 

494 check against user quota, 

495 e.g. {'instances': 1, 'cores': 2, 'memory_mb': 512} 

496 :param project_id: Optional project_id for scoping the limit check to a 

497 different project than in the context 

498 :param user_id: Optional user_id for scoping the limit check to a 

499 different user than in the context 

500 """ 

501 if project_values is None: 

502 project_values = {} 

503 if user_values is None: 

504 user_values = {} 

505 

506 _valid_method_call_check_resources(project_values, 'check', resources) 

507 _valid_method_call_check_resources(user_values, 'check', resources) 

508 

509 if not any([project_values, user_values]): 

510 raise exception.Invalid( 

511 'Must specify at least one of project_values or user_values ' 

512 'for the limit check.') 

513 

514 # Ensure no value is less than zero 

515 for vals in (project_values, user_values): 

516 unders = [key for key, val in vals.items() if val < 0] 

517 if unders: 

518 raise exception.InvalidQuotaValue(unders=sorted(unders)) 

519 

520 # Get a set of all keys for calling _get_quotas() so we get all of the 

521 # resource limits we need. 

522 all_keys = set(project_values).union(user_values) 

523 

524 # Keys that are in both project_values and user_values need to be 

525 # checked against project quota and user quota, respectively. 

526 # Keys that are not in both only need to be checked against project 

527 # quota or user quota, if it is defined. Separate the keys that don't 

528 # need to be checked against both quotas, merge them into one dict, 

529 # and remove them from project_values and user_values. 

530 keys_to_merge = set(project_values).symmetric_difference(user_values) 

531 merged_values = {} 

532 for key in keys_to_merge: 

533 # The key will be either in project_values or user_values based on 

534 # the earlier symmetric_difference. Default to 0 in case the found 

535 # value is 0 and won't take precedence over a None default. 

536 merged_values[key] = (project_values.get(key, 0) or 

537 user_values.get(key, 0)) 

538 project_values.pop(key, None) 

539 user_values.pop(key, None) 

540 

541 # If project_id is None, then we use the project_id in context 

542 if project_id is None: 

543 project_id = context.project_id 

544 # If user id is None, then we use the user_id in context 

545 if user_id is None: 

546 user_id = context.user_id 

547 

548 # Get the applicable quotas. They will be merged together (taking the 

549 # min limit) if project_values and user_values were not specified 

550 # together. 

551 

552 # per project quota limits (quotas that have no concept of 

553 # user-scoping: <none>) 

554 project_quotas = objects.Quotas.get_all_by_project(context, project_id) 

555 # per user quotas, project quota limits (for quotas that have 

556 # user-scoping, limits for the project) 

557 quotas = self._get_quotas(context, resources, all_keys, 

558 project_id=project_id, 

559 project_quotas=project_quotas) 

560 # per user quotas, user quota limits (for quotas that have 

561 # user-scoping, the limits for the user) 

562 user_quotas = self._get_quotas(context, resources, all_keys, 

563 project_id=project_id, 

564 user_id=user_id, 

565 project_quotas=project_quotas) 

566 

567 if merged_values: 

568 # This is for resources that are not counted across a project and 

569 # must pass both the quota for the project and the quota for the 

570 # user. 

571 # Combine per user project quotas and user_quotas for use in the 

572 # checks, taking the minimum limit between the two. 

573 merged_quotas = copy.deepcopy(quotas) 

574 for k, v in user_quotas.items(): 

575 if k in merged_quotas: 575 ↛ 578line 575 didn't jump to line 578 because the condition on line 575 was always true

576 merged_quotas[k] = min(merged_quotas[k], v) 

577 else: 

578 merged_quotas[k] = v 

579 

580 # Check the quotas and construct a list of the resources that 

581 # would be put over limit by the desired values 

582 overs = [key for key, val in merged_values.items() 

583 if merged_quotas[key] >= 0 and merged_quotas[key] < val] 

584 if overs: 

585 headroom = {} 

586 for key in overs: 

587 headroom[key] = merged_quotas[key] 

588 raise exception.OverQuota(overs=sorted(overs), 

589 quotas=merged_quotas, usages={}, 

590 headroom=headroom) 

591 

592 # This is for resources that are counted across a project and 

593 # across a user (instances, cores, ram, server_groups). The 

594 # project_values must pass the quota for the project and the 

595 # user_values must pass the quota for the user. 

596 over_user_quota = False 

597 overs = [] 

598 for key in user_values.keys(): 

599 # project_values and user_values should contain the same keys or 

600 # be empty after the keys in the symmetric_difference were removed 

601 # from both dicts. 

602 if quotas[key] >= 0 and quotas[key] < project_values[key]: 

603 overs.append(key) 

604 elif (user_quotas[key] >= 0 and 

605 user_quotas[key] < user_values[key]): 

606 overs.append(key) 

607 over_user_quota = True 

608 if overs: 

609 quotas_exceeded = user_quotas if over_user_quota else quotas 

610 headroom = {} 

611 for key in overs: 

612 headroom[key] = quotas_exceeded[key] 

613 raise exception.OverQuota(overs=sorted(overs), 

614 quotas=quotas_exceeded, usages={}, 

615 headroom=headroom) 

616 

617 

618class NoopQuotaDriver(object): 

619 """Driver that turns quotas calls into no-ops and pretends that quotas 

620 for all resources are unlimited. This can be used if you do not 

621 wish to have any quota checking. 

622 """ 

623 

624 def get_reserved(self): 

625 # Noop has always returned -1 for reserved 

626 return -1 

627 

628 def get_defaults(self, context, resources): 

629 """Given a list of resources, retrieve the default quotas. 

630 

631 :param context: The request context, for access checks. 

632 :param resources: A dictionary of the registered resources. 

633 """ 

634 quotas = {} 

635 for resource in resources.values(): 

636 quotas[resource.name] = -1 

637 return quotas 

638 

639 def get_class_quotas(self, context, resources, quota_class): 

640 """Given a list of resources, retrieve the quotas for the given 

641 quota class. 

642 

643 :param context: The request context, for access checks. 

644 :param resources: A dictionary of the registered resources. 

645 :param quota_class: The name of the quota class to return 

646 quotas for. 

647 """ 

648 quotas = {} 

649 for resource in resources.values(): 

650 quotas[resource.name] = -1 

651 return quotas 

652 

653 def _get_noop_quotas(self, resources, usages=None, remains=False): 

654 quotas = {} 

655 for resource in resources.values(): 

656 quotas[resource.name] = {} 

657 quotas[resource.name]['limit'] = -1 

658 if usages: 

659 quotas[resource.name]['in_use'] = -1 

660 if remains: 660 ↛ 661line 660 didn't jump to line 661 because the condition on line 660 was never true

661 quotas[resource.name]['remains'] = -1 

662 return quotas 

663 

664 def get_user_quotas(self, context, resources, project_id, user_id, 

665 quota_class=None, 

666 usages=True): 

667 """Given a list of resources, retrieve the quotas for the given 

668 user and project. 

669 

670 :param context: The request context, for access checks. 

671 :param resources: A dictionary of the registered resources. 

672 :param project_id: The ID of the project to return quotas for. 

673 :param user_id: The ID of the user to return quotas for. 

674 :param quota_class: If project_id != context.project_id, the 

675 quota class cannot be determined. This 

676 parameter allows it to be specified. It 

677 will be ignored if project_id == 

678 context.project_id. 

679 :param usages: If True, the current counts will also be returned. 

680 """ 

681 return self._get_noop_quotas(resources, usages=usages) 

682 

683 def get_project_quotas(self, context, resources, project_id, 

684 quota_class=None, 

685 usages=True, remains=False): 

686 """Given a list of resources, retrieve the quotas for the given 

687 project. 

688 

689 :param context: The request context, for access checks. 

690 :param resources: A dictionary of the registered resources. 

691 :param project_id: The ID of the project to return quotas for. 

692 :param quota_class: If project_id != context.project_id, the 

693 quota class cannot be determined. This 

694 parameter allows it to be specified. It 

695 will be ignored if project_id == 

696 context.project_id. 

697 :param usages: If True, the current counts will also be returned. 

698 :param remains: If True, the current remains of the project will 

699 will be returned. 

700 """ 

701 return self._get_noop_quotas(resources, usages=usages, remains=remains) 

702 

703 def get_settable_quotas(self, context, resources, project_id, 

704 user_id=None): 

705 """Given a list of resources, retrieve the range of settable quotas for 

706 the given user or project. 

707 

708 :param context: The request context, for access checks. 

709 :param resources: A dictionary of the registered resources. 

710 :param project_id: The ID of the project to return quotas for. 

711 :param user_id: The ID of the user to return quotas for. 

712 """ 

713 quotas = {} 

714 for resource in resources.values(): 

715 quotas[resource.name] = {'minimum': 0, 'maximum': -1} 

716 return quotas 

717 

718 def limit_check(self, context, resources, values, project_id=None, 

719 user_id=None): 

720 """Check simple quota limits. 

721 

722 For limits--those quotas for which there is no usage 

723 synchronization function--this method checks that a set of 

724 proposed values are permitted by the limit restriction. 

725 

726 This method will raise a QuotaResourceUnknown exception if a 

727 given resource is unknown or if it is not a simple limit 

728 resource. 

729 

730 If any of the proposed values is over the defined quota, an 

731 OverQuota exception will be raised with the sorted list of the 

732 resources which are too high. Otherwise, the method returns 

733 nothing. 

734 

735 :param context: The request context, for access checks. 

736 :param resources: A dictionary of the registered resources. 

737 :param values: A dictionary of the values to check against the 

738 quota. 

739 :param project_id: Specify the project_id if current context 

740 is admin and admin wants to impact on 

741 common user's tenant. 

742 :param user_id: Specify the user_id if current context 

743 is admin and admin wants to impact on 

744 common user. 

745 """ 

746 pass 

747 

748 def limit_check_project_and_user(self, context, resources, 

749 project_values=None, user_values=None, 

750 project_id=None, user_id=None): 

751 """Check values against quota limits. 

752 

753 For limits--this method checks that a set of 

754 proposed values are permitted by the limit restriction. 

755 

756 This method will raise a QuotaResourceUnknown exception if a 

757 given resource is unknown or if it is not a simple limit 

758 resource. 

759 

760 If any of the proposed values is over the defined quota, an 

761 OverQuota exception will be raised with the sorted list of the 

762 resources which are too high. Otherwise, the method returns 

763 nothing. 

764 

765 :param context: The request context, for access checks 

766 :param resources: A dictionary of the registered resources 

767 :param project_values: Optional dict containing the resource values to 

768 check against project quota, 

769 e.g. {'instances': 1, 'cores': 2, 'memory_mb': 512} 

770 :param user_values: Optional dict containing the resource values to 

771 check against user quota, 

772 e.g. {'instances': 1, 'cores': 2, 'memory_mb': 512} 

773 :param project_id: Optional project_id for scoping the limit check to a 

774 different project than in the context 

775 :param user_id: Optional user_id for scoping the limit check to a 

776 different user than in the context 

777 """ 

778 pass 

779 

780 

781class UnifiedLimitsDriver(NoopQuotaDriver): 

782 """Ease migration to new unified limits code. 

783 

784 Help ease migration to unified limits by ensuring the old code 

785 paths still work with unified limits. Eventually the expectation is 

786 all this legacy quota code will go away, leaving the new simpler code 

787 """ 

788 

789 def __init__(self): 

790 LOG.warning("The Unified Limits Quota Driver is experimental and " 

791 "is under active development. Do not use this driver.") 

792 

793 def get_reserved(self): 

794 # To make unified limits APIs the same as the DB driver, return 0 

795 return 0 

796 

797 def get_class_quotas(self, context, resources, quota_class): 

798 """Given a list of resources, retrieve the quotas for the given 

799 quota class. 

800 

801 :param context: The request context, for access checks. 

802 :param resources: A dictionary of the registered resources. 

803 :param quota_class: Placeholder, we always assume default quota class. 

804 """ 

805 # NOTE(johngarbutt): ignoring quota_class, as ignored in noop driver 

806 return self.get_defaults(context, resources) 

807 

808 def get_defaults(self, context, resources): 

809 local_limits = local_limit.get_legacy_default_limits() 

810 # Note we get 0 if there is no registered limit, 

811 # to mirror oslo_limit behaviour when there is no registered limit 

812 placement_limits = placement_limit.get_legacy_default_limits() 

813 quotas = {} 

814 for resource in resources.values(): 

815 if resource.name in placement_limits: 

816 quotas[resource.name] = placement_limits[resource.name] 

817 else: 

818 # return -1 for things like security_group_rules 

819 # that are neither a keystone limit or a local limit 

820 quotas[resource.name] = local_limits.get(resource.name, -1) 

821 

822 return quotas 

823 

824 def get_project_quotas(self, context, resources, project_id, 

825 quota_class=None, 

826 usages=True, remains=False): 

827 if quota_class is not None: 827 ↛ 828line 827 didn't jump to line 828 because the condition on line 827 was never true

828 raise NotImplementedError("quota_class") 

829 

830 if remains: 830 ↛ 831line 830 didn't jump to line 831 because the condition on line 830 was never true

831 raise NotImplementedError("remains") 

832 

833 local_limits = local_limit.get_legacy_default_limits() 

834 # keystone limits always returns core, ram and instances 

835 # if nothing set in keystone, we get back 0, i.e. don't allow 

836 placement_limits = placement_limit.get_legacy_project_limits( 

837 project_id) 

838 

839 project_quotas = {} 

840 for resource in resources.values(): 

841 if resource.name in placement_limits: 

842 limit = placement_limits[resource.name] 

843 else: 

844 # return -1 for things like security_group_rules 

845 # that are neither a keystone limit or a local limit 

846 limit = local_limits.get(resource.name, -1) 

847 project_quotas[resource.name] = {"limit": limit} 

848 

849 if usages: 

850 local_in_use = local_limit.get_in_use(context, project_id) 

851 p_in_use = placement_limit.get_legacy_counts(context, project_id) 

852 

853 for resource in resources.values(): 

854 # default to 0 for resources that are deprecated, 

855 # i.e. not in keystone or local limits, such that we 

856 # are API compatible with what was returned with 

857 # the db driver, even though noop driver returned -1 

858 usage_count = 0 

859 if resource.name in local_in_use: 

860 usage_count = local_in_use[resource.name] 

861 if resource.name in p_in_use: 

862 usage_count = p_in_use[resource.name] 

863 project_quotas[resource.name]["in_use"] = usage_count 

864 

865 return project_quotas 

866 

867 def get_user_quotas(self, context, resources, project_id, user_id, 

868 quota_class=None, usages=True): 

869 return self.get_project_quotas(context, resources, project_id, 

870 quota_class, usages) 

871 

872 

873class BaseResource(object): 

874 """Describe a single resource for quota checking.""" 

875 

876 def __init__(self, name, flag=None): 

877 """Initializes a Resource. 

878 

879 :param name: The name of the resource, i.e., "instances". 

880 :param flag: The name of the flag or configuration option 

881 which specifies the default value of the quota 

882 for this resource. 

883 """ 

884 

885 self.name = name 

886 self.flag = flag 

887 

888 @property 

889 def default(self): 

890 """Return the default value of the quota.""" 

891 return CONF.quota[self.flag] if self.flag else -1 

892 

893 

894class AbsoluteResource(BaseResource): 

895 """Describe a resource that does not correspond to database objects.""" 

896 valid_method = 'check' 

897 

898 

899class CountableResource(AbsoluteResource): 

900 """Describe a resource where the counts aren't based solely on the 

901 project ID. 

902 """ 

903 

904 def __init__(self, name, count_as_dict, flag=None): 

905 """Initializes a CountableResource. 

906 

907 Countable resources are those resources which directly 

908 correspond to objects in the database, but for which a count 

909 by project ID is inappropriate e.g. keypairs 

910 A CountableResource must be constructed with a counting 

911 function, which will be called to determine the current counts 

912 of the resource. 

913 

914 The counting function will be passed the context, along with 

915 the extra positional and keyword arguments that are passed to 

916 Quota.count_as_dict(). It should return a dict specifying the 

917 count scoped to a project and/or a user. 

918 

919 Example count of instances, cores, or ram returned as a rollup 

920 of all the resources since we only want to query the instances 

921 table once, not multiple times, for each resource. 

922 Instances, cores, and ram are counted across a project and 

923 across a user: 

924 

925 {'project': {'instances': 5, 'cores': 8, 'ram': 4096}, 

926 'user': {'instances': 1, 'cores': 2, 'ram': 512}} 

927 

928 Example count of server groups keeping a consistent format. 

929 Server groups are counted across a project and across a user: 

930 

931 {'project': {'server_groups': 7}, 

932 'user': {'server_groups': 2}} 

933 

934 Example count of key pairs keeping a consistent format. 

935 Key pairs are counted across a user only: 

936 

937 {'user': {'key_pairs': 5}} 

938 

939 Note that this counting is not performed in a transaction-safe 

940 manner. This resource class is a temporary measure to provide 

941 required functionality, until a better approach to solving 

942 this problem can be evolved. 

943 

944 :param name: The name of the resource, i.e., "instances". 

945 :param count_as_dict: A callable which returns the count of the 

946 resource as a dict. The arguments passed are as 

947 described above. 

948 :param flag: The name of the flag or configuration option 

949 which specifies the default value of the quota 

950 for this resource. 

951 """ 

952 

953 super(CountableResource, self).__init__(name, flag=flag) 

954 self.count_as_dict = count_as_dict 

955 

956 

957class QuotaEngine(object): 

958 """Represent the set of recognized quotas.""" 

959 

960 def __init__(self, quota_driver=None, resources=None): 

961 """Initialize a Quota object. 

962 

963 :param quota_driver: a QuotaDriver object (only used in testing. if 

964 None (default), instantiates a driver from the 

965 CONF.quota.driver option) 

966 :param resources: iterable of Resource objects 

967 """ 

968 resources = resources or [] 

969 self._resources = { 

970 resource.name: resource for resource in resources 

971 } 

972 # NOTE(mriedem): quota_driver is ever only supplied in tests with a 

973 # fake driver. 

974 self.__driver_override = quota_driver 

975 self.__driver = None 

976 self.__driver_name = None 

977 

978 @property 

979 def _driver(self): 

980 if self.__driver_override: 

981 return self.__driver_override 

982 

983 # NOTE(johngarbutt) to allow unit tests to change the driver by 

984 # simply overriding config, double check if we have the correct 

985 # driver cached before we return the currently cached driver 

986 driver_name_in_config = CONF.quota.driver 

987 if self.__driver_name != driver_name_in_config: 

988 self.__driver = importutils.import_object(driver_name_in_config) 

989 self.__driver_name = driver_name_in_config 

990 

991 return self.__driver 

992 

993 def get_defaults(self, context): 

994 """Retrieve the default quotas. 

995 

996 :param context: The request context, for access checks. 

997 """ 

998 

999 return self._driver.get_defaults(context, self._resources) 

1000 

1001 def get_class_quotas(self, context, quota_class): 

1002 """Retrieve the quotas for the given quota class. 

1003 

1004 :param context: The request context, for access checks. 

1005 :param quota_class: The name of the quota class to return 

1006 quotas for. 

1007 """ 

1008 

1009 return self._driver.get_class_quotas(context, self._resources, 

1010 quota_class) 

1011 

1012 def get_user_quotas(self, context, project_id, user_id, quota_class=None, 

1013 usages=True): 

1014 """Retrieve the quotas for the given user and project. 

1015 

1016 :param context: The request context, for access checks. 

1017 :param project_id: The ID of the project to return quotas for. 

1018 :param user_id: The ID of the user to return quotas for. 

1019 :param quota_class: If project_id != context.project_id, the 

1020 quota class cannot be determined. This 

1021 parameter allows it to be specified. 

1022 :param usages: If True, the current counts will also be returned. 

1023 """ 

1024 

1025 return self._driver.get_user_quotas(context, self._resources, 

1026 project_id, user_id, 

1027 quota_class=quota_class, 

1028 usages=usages) 

1029 

1030 def get_project_quotas(self, context, project_id, quota_class=None, 

1031 usages=True, remains=False): 

1032 """Retrieve the quotas for the given project. 

1033 

1034 :param context: The request context, for access checks. 

1035 :param project_id: The ID of the project to return quotas for. 

1036 :param quota_class: If project_id != context.project_id, the 

1037 quota class cannot be determined. This 

1038 parameter allows it to be specified. 

1039 :param usages: If True, the current counts will also be returned. 

1040 :param remains: If True, the current remains of the project will 

1041 will be returned. 

1042 """ 

1043 

1044 return self._driver.get_project_quotas(context, self._resources, 

1045 project_id, 

1046 quota_class=quota_class, 

1047 usages=usages, 

1048 remains=remains) 

1049 

1050 def get_settable_quotas(self, context, project_id, user_id=None): 

1051 """Given a list of resources, retrieve the range of settable quotas for 

1052 the given user or project. 

1053 

1054 :param context: The request context, for access checks. 

1055 :param project_id: The ID of the project to return quotas for. 

1056 :param user_id: The ID of the user to return quotas for. 

1057 """ 

1058 

1059 return self._driver.get_settable_quotas(context, self._resources, 

1060 project_id, 

1061 user_id=user_id) 

1062 

1063 def count_as_dict(self, context, resource, *args, **kwargs): 

1064 """Count a resource and return a dict. 

1065 

1066 For countable resources, invokes the count_as_dict() function and 

1067 returns its result. Arguments following the context and 

1068 resource are passed directly to the count function declared by 

1069 the resource. 

1070 

1071 :param context: The request context, for access checks. 

1072 :param resource: The name of the resource, as a string. 

1073 :returns: A dict containing the count(s) for the resource, for example: 

1074 {'project': {'instances': 2, 'cores': 4, 'ram': 1024}, 

1075 'user': {'instances': 1, 'cores': 2, 'ram': 512}} 

1076 

1077 another example: 

1078 {'user': {'key_pairs': 5}} 

1079 """ 

1080 

1081 # Get the resource 

1082 res = self._resources.get(resource) 

1083 if not res or not hasattr(res, 'count_as_dict'): 

1084 raise exception.QuotaResourceUnknown(unknown=[resource]) 

1085 

1086 return res.count_as_dict(context, *args, **kwargs) 

1087 

1088 # TODO(melwitt): This can be removed once no old code can call 

1089 # limit_check(). It will be replaced with limit_check_project_and_user(). 

1090 def limit_check(self, context, project_id=None, user_id=None, **values): 

1091 """Check simple quota limits. 

1092 

1093 For limits--those quotas for which there is no usage 

1094 synchronization function--this method checks that a set of 

1095 proposed values are permitted by the limit restriction. The 

1096 values to check are given as keyword arguments, where the key 

1097 identifies the specific quota limit to check, and the value is 

1098 the proposed value. 

1099 

1100 This method will raise a QuotaResourceUnknown exception if a 

1101 given resource is unknown or if it is not a simple limit 

1102 resource. 

1103 

1104 If any of the proposed values is over the defined quota, an 

1105 OverQuota exception will be raised with the sorted list of the 

1106 resources which are too high. Otherwise, the method returns 

1107 nothing. 

1108 

1109 :param context: The request context, for access checks. 

1110 :param project_id: Specify the project_id if current context 

1111 is admin and admin wants to impact on 

1112 common user's tenant. 

1113 :param user_id: Specify the user_id if current context 

1114 is admin and admin wants to impact on 

1115 common user. 

1116 """ 

1117 

1118 return self._driver.limit_check(context, self._resources, values, 

1119 project_id=project_id, user_id=user_id) 

1120 

1121 def limit_check_project_and_user(self, context, project_values=None, 

1122 user_values=None, project_id=None, 

1123 user_id=None): 

1124 """Check values against quota limits. 

1125 

1126 For limits--this method checks that a set of 

1127 proposed values are permitted by the limit restriction. 

1128 

1129 This method will raise a QuotaResourceUnknown exception if a 

1130 given resource is unknown or if it is not a simple limit 

1131 resource. 

1132 

1133 If any of the proposed values is over the defined quota, an 

1134 OverQuota exception will be raised with the sorted list of the 

1135 resources which are too high. Otherwise, the method returns 

1136 nothing. 

1137 

1138 :param context: The request context, for access checks 

1139 :param project_values: Optional dict containing the resource values to 

1140 check against project quota, 

1141 e.g. {'instances': 1, 'cores': 2, 'memory_mb': 512} 

1142 :param user_values: Optional dict containing the resource values to 

1143 check against user quota, 

1144 e.g. {'instances': 1, 'cores': 2, 'memory_mb': 512} 

1145 :param project_id: Optional project_id for scoping the limit check to a 

1146 different project than in the context 

1147 :param user_id: Optional user_id for scoping the limit check to a 

1148 different user than in the context 

1149 """ 

1150 return self._driver.limit_check_project_and_user( 

1151 context, self._resources, project_values=project_values, 

1152 user_values=user_values, project_id=project_id, user_id=user_id) 

1153 

1154 @property 

1155 def resources(self): 

1156 return sorted(self._resources.keys()) 

1157 

1158 def get_reserved(self): 

1159 return self._driver.get_reserved() 

1160 

1161 

1162@api_db_api.context_manager.reader 

1163def _user_id_queued_for_delete_populated(context, project_id=None): 

1164 """Determine whether user_id and queued_for_delete are set. 

1165 

1166 This will be used to determine whether we need to fall back on 

1167 the legacy quota counting method (if we cannot rely on counting 

1168 instance mappings for the instance count). If any records with user_id=None 

1169 and queued_for_delete=False are found, we need to fall back to the legacy 

1170 counting method. If any records with queued_for_delete=None are found, we 

1171 need to fall back to the legacy counting method. 

1172 

1173 Note that this check specifies queued_for_deleted=False, which excludes 

1174 deleted and SOFT_DELETED instances. The 'populate_user_id' data migration 

1175 migrates SOFT_DELETED instances because they could be restored at any time 

1176 in the future. However, for this quota-check-time method, it is acceptable 

1177 to ignore SOFT_DELETED instances, since we just want to know if it is safe 

1178 to use instance mappings to count instances at this point in time (and 

1179 SOFT_DELETED instances do not count against quota limits). 

1180 

1181 We also want to fall back to the legacy counting method if we detect any 

1182 records that have not yet populated the queued_for_delete field. We do this 

1183 instead of counting queued_for_delete=None records since that might not 

1184 accurately reflect the project or project user's quota usage. 

1185 

1186 :param project_id: The project to check 

1187 :returns: True if user_id is set for all non-deleted instances and 

1188 queued_for_delete is set for all instances, else False 

1189 """ 

1190 user_id_not_populated = sql.and_( 

1191 api_models.InstanceMapping.user_id == sql.null(), 

1192 api_models.InstanceMapping.queued_for_delete == sql.false()) 

1193 # If either queued_for_delete or user_id are unmigrated, we will return 

1194 # False. 

1195 unmigrated_filter = sql.or_( 

1196 api_models.InstanceMapping.queued_for_delete == sql.null(), 

1197 user_id_not_populated) 

1198 query = context.session.query(api_models.InstanceMapping).filter( 

1199 unmigrated_filter) 

1200 if project_id: 1200 ↛ 1201line 1200 didn't jump to line 1201 because the condition on line 1200 was never true

1201 query = query.filter_by(project_id=project_id) 

1202 return not context.session.query(query.exists()).scalar() 

1203 

1204 

1205def _keypair_get_count_by_user(context, user_id): 

1206 count = objects.KeyPairList.get_count_by_user(context, user_id) 

1207 return {'user': {'key_pairs': count}} 

1208 

1209 

1210def _server_group_count_members_by_user_legacy(context, group, user_id): 

1211 # NOTE(melwitt): This is mostly duplicated from 

1212 # InstanceGroup.count_members_by_user() to query across multiple cells. 

1213 # We need to be able to pass the correct cell context to 

1214 # InstanceList.get_by_filters(). 

1215 # NOTE(melwitt): Counting across cells for instances means we will miss 

1216 # counting resources if a cell is down. 

1217 cell_mappings = objects.CellMappingList.get_all(context) 

1218 greenthreads = [] 

1219 filters = {'deleted': False, 'user_id': user_id, 'uuid': group.members} 

1220 for cell_mapping in cell_mappings: 

1221 with nova_context.target_cell(context, cell_mapping) as cctxt: 

1222 greenthreads.append(utils.spawn( 

1223 objects.InstanceList.get_by_filters, cctxt, filters, 

1224 expected_attrs=[])) 

1225 instances = objects.InstanceList(objects=[]) 

1226 for greenthread in greenthreads: 

1227 found = greenthread.wait() 

1228 instances = instances + found 

1229 # Count build requests using the same filters to catch group members 

1230 # that are not yet created in a cell. 

1231 # NOTE(mriedem): BuildRequestList.get_by_filters is not very efficient for 

1232 # what we need and we can optimize this with a new query method. 

1233 build_requests = objects.BuildRequestList.get_by_filters(context, filters) 

1234 # Ignore any duplicates since build requests and instances can co-exist 

1235 # for a short window of time after the instance is created in a cell but 

1236 # before the build request is deleted. 

1237 instance_uuids = [inst.uuid for inst in instances] 

1238 count = len(instances) 

1239 for build_request in build_requests: 

1240 if build_request.instance_uuid not in instance_uuids: 

1241 count += 1 

1242 return {'user': {'server_group_members': count}} 

1243 

1244 

1245def is_qfd_populated(context): 

1246 """Check if user_id and queued_for_delete fields are populated. 

1247 

1248 This method is related to counting quota usage from placement. It is not 

1249 yet possible to count instances from placement, so in the meantime we can 

1250 use instance mappings for counting. This method is used to determine 

1251 whether the user_id and queued_for_delete columns are populated in the API 

1252 database's instance_mappings table. Instance mapping records are not 

1253 deleted from the database until the database is archived, so 

1254 queued_for_delete tells us whether or not we should count them for instance 

1255 quota usage. The user_id field enables us to scope instance quota usage to 

1256 a user (legacy quota). 

1257 

1258 Scoping instance quota to a user is only possible 

1259 when counting quota usage from placement is configured and unified limits 

1260 is not configured. When unified limits is configured, quotas are scoped 

1261 only to projects. 

1262 

1263 In the future when it is possible to count instance usage from placement, 

1264 this method will no longer be needed. 

1265 """ 

1266 global UID_QFD_POPULATED_CACHE_ALL 

1267 if not UID_QFD_POPULATED_CACHE_ALL: 

1268 LOG.debug('Checking whether user_id and queued_for_delete are ' 

1269 'populated for all projects') 

1270 UID_QFD_POPULATED_CACHE_ALL = _user_id_queued_for_delete_populated( 

1271 context) 

1272 

1273 return UID_QFD_POPULATED_CACHE_ALL 

1274 

1275 

1276def _server_group_count_members_by_user(context, group, user_id): 

1277 """Get the count of server group members for a group by user. 

1278 

1279 :param context: The request context for database access 

1280 :param group: The InstanceGroup object with members to count 

1281 :param user_id: The user_id to count across 

1282 :returns: A dict containing the user-scoped count. For example: 

1283 

1284 {'user': 'server_group_members': <count across user>}} 

1285 """ 

1286 # Because server group members quota counting is not scoped to a project, 

1287 # but scoped to a particular InstanceGroup and user, we have no reasonable 

1288 # way of pruning down our migration check to only a subset of all instance 

1289 # mapping records. 

1290 # So, we check whether user_id/queued_for_delete is populated for all 

1291 # records and cache the result to prevent unnecessary checking once the 

1292 # data migration has been completed. 

1293 if is_qfd_populated(context): 1293 ↛ 1298line 1293 didn't jump to line 1298 because the condition on line 1293 was always true

1294 count = objects.InstanceMappingList.get_count_by_uuids_and_user( 

1295 context, group.members, user_id) 

1296 return {'user': {'server_group_members': count}} 

1297 

1298 LOG.warning('Falling back to legacy quota counting method for server ' 

1299 'group members') 

1300 return _server_group_count_members_by_user_legacy(context, group, 

1301 user_id) 

1302 

1303 

1304def _instances_cores_ram_count_legacy(context, project_id, user_id=None): 

1305 """Get the counts of instances, cores, and ram in cell databases. 

1306 

1307 :param context: The request context for database access 

1308 :param project_id: The project_id to count across 

1309 :param user_id: The user_id to count across 

1310 :returns: A dict containing the project-scoped counts and user-scoped 

1311 counts if user_id is specified. For example: 

1312 

1313 {'project': {'instances': <count across project>, 

1314 'cores': <count across project>, 

1315 'ram': <count across project>}, 

1316 'user': {'instances': <count across user>, 

1317 'cores': <count across user>, 

1318 'ram': <count across user>}} 

1319 """ 

1320 # NOTE(melwitt): Counting across cells for instances, cores, and ram means 

1321 # we will miss counting resources if a cell is down. 

1322 # NOTE(tssurya): We only go into those cells in which the tenant has 

1323 # instances. We could optimize this to avoid the CellMappingList query 

1324 # for single-cell deployments by checking the cell cache and only doing 

1325 # this filtering if there is more than one non-cell0 cell. 

1326 # TODO(tssurya): Consider adding a scatter_gather_cells_for_project 

1327 # variant that makes this native to nova.context. 

1328 if CONF.api.instance_list_per_project_cells: 1328 ↛ 1329line 1328 didn't jump to line 1329 because the condition on line 1328 was never true

1329 cell_mappings = objects.CellMappingList.get_by_project_id( 

1330 context, project_id) 

1331 else: 

1332 nova_context.load_cells() 

1333 cell_mappings = nova_context.CELLS 

1334 results = nova_context.scatter_gather_cells( 

1335 context, cell_mappings, nova_context.CELL_TIMEOUT, 

1336 objects.InstanceList.get_counts, project_id, user_id=user_id) 

1337 total_counts = {'project': {'instances': 0, 'cores': 0, 'ram': 0}} 

1338 if user_id: 

1339 total_counts['user'] = {'instances': 0, 'cores': 0, 'ram': 0} 

1340 for result in results.values(): 

1341 if not nova_context.is_cell_failure_sentinel(result): 1341 ↛ 1340line 1341 didn't jump to line 1340 because the condition on line 1341 was always true

1342 for resource, count in result['project'].items(): 

1343 total_counts['project'][resource] += count 

1344 if user_id: 

1345 for resource, count in result['user'].items(): 

1346 total_counts['user'][resource] += count 

1347 return total_counts 

1348 

1349 

1350def _cores_ram_count_placement(context, project_id, user_id=None): 

1351 return report.report_client_singleton().get_usages_counts_for_quota( 

1352 context, project_id, user_id=user_id) 

1353 

1354 

1355def _instances_cores_ram_count_api_db_placement(context, project_id, 

1356 user_id=None): 

1357 # Will return a dict with format: {'project': {'instances': M}, 

1358 # 'user': {'instances': N}} 

1359 # where the 'user' key is optional. 

1360 total_counts = objects.InstanceMappingList.get_counts(context, 

1361 project_id, 

1362 user_id=user_id) 

1363 cores_ram_counts = _cores_ram_count_placement(context, project_id, 

1364 user_id=user_id) 

1365 total_counts['project'].update(cores_ram_counts['project']) 

1366 if 'user' in total_counts: 1366 ↛ 1368line 1366 didn't jump to line 1368 because the condition on line 1366 was always true

1367 total_counts['user'].update(cores_ram_counts['user']) 

1368 return total_counts 

1369 

1370 

1371def _instances_cores_ram_count(context, project_id, user_id=None): 

1372 """Get the counts of instances, cores, and ram. 

1373 

1374 :param context: The request context for database access 

1375 :param project_id: The project_id to count across 

1376 :param user_id: The user_id to count across 

1377 :returns: A dict containing the project-scoped counts and user-scoped 

1378 counts if user_id is specified. For example: 

1379 

1380 {'project': {'instances': <count across project>, 

1381 'cores': <count across project>, 

1382 'ram': <count across project>}, 

1383 'user': {'instances': <count across user>, 

1384 'cores': <count across user>, 

1385 'ram': <count across user>}} 

1386 """ 

1387 global UID_QFD_POPULATED_CACHE_BY_PROJECT 

1388 if CONF.quota.count_usage_from_placement: 

1389 # If a project has all user_id and queued_for_delete data populated, 

1390 # cache the result to avoid needless database checking in the future. 

1391 if (not UID_QFD_POPULATED_CACHE_ALL and 

1392 project_id not in UID_QFD_POPULATED_CACHE_BY_PROJECT): 

1393 LOG.debug('Checking whether user_id and queued_for_delete are ' 

1394 'populated for project_id %s', project_id) 

1395 uid_qfd_populated = _user_id_queued_for_delete_populated( 

1396 context, project_id) 

1397 if uid_qfd_populated: 

1398 UID_QFD_POPULATED_CACHE_BY_PROJECT.add(project_id) 

1399 else: 

1400 uid_qfd_populated = True 

1401 if uid_qfd_populated: 

1402 return _instances_cores_ram_count_api_db_placement(context, 

1403 project_id, 

1404 user_id=user_id) 

1405 LOG.warning('Falling back to legacy quota counting method for ' 

1406 'instances, cores, and ram') 

1407 return _instances_cores_ram_count_legacy(context, project_id, 

1408 user_id=user_id) 

1409 

1410 

1411def _server_group_count(context, project_id, user_id=None): 

1412 """Get the counts of server groups in the database. 

1413 

1414 :param context: The request context for database access 

1415 :param project_id: The project_id to count across 

1416 :param user_id: The user_id to count across 

1417 :returns: A dict containing the project-scoped counts and user-scoped 

1418 counts if user_id is specified. For example: 

1419 

1420 {'project': {'server_groups': <count across project>}, 

1421 'user': {'server_groups': <count across user>}} 

1422 """ 

1423 return objects.InstanceGroupList.get_counts(context, project_id, 

1424 user_id=user_id) 

1425 

1426 

1427QUOTAS = QuotaEngine( 

1428 resources=[ 

1429 CountableResource( 

1430 'instances', _instances_cores_ram_count, 'instances'), 

1431 CountableResource( 

1432 'cores', _instances_cores_ram_count, 'cores'), 

1433 CountableResource( 

1434 'ram', _instances_cores_ram_count, 'ram'), 

1435 AbsoluteResource( 

1436 'metadata_items', 'metadata_items'), 

1437 AbsoluteResource( 

1438 'injected_files', 'injected_files'), 

1439 AbsoluteResource( 

1440 'injected_file_content_bytes', 'injected_file_content_bytes'), 

1441 AbsoluteResource( 

1442 'injected_file_path_bytes', 'injected_file_path_length'), 

1443 CountableResource( 

1444 'key_pairs', _keypair_get_count_by_user, 'key_pairs'), 

1445 CountableResource( 

1446 'server_groups', _server_group_count, 'server_groups'), 

1447 CountableResource( 

1448 'server_group_members', _server_group_count_members_by_user, 

1449 'server_group_members'), 

1450 # Deprecated nova-network quotas, retained to avoid changing API 

1451 # responses 

1452 AbsoluteResource('fixed_ips'), 

1453 AbsoluteResource('floating_ips'), 

1454 AbsoluteResource('security_groups'), 

1455 AbsoluteResource('security_group_rules'), 

1456 ], 

1457) 

1458 

1459 

1460def _valid_method_call_check_resource(name, method, resources): 

1461 if name not in resources: 

1462 raise exception.InvalidQuotaMethodUsage(method=method, res=name) 

1463 res = resources[name] 

1464 

1465 if res.valid_method != method: 

1466 raise exception.InvalidQuotaMethodUsage(method=method, res=name) 

1467 

1468 

1469def _valid_method_call_check_resources(resource_values, method, resources): 

1470 """A method to check whether the resource can use the quota method. 

1471 

1472 :param resource_values: Dict containing the resource names and values 

1473 :param method: The quota method to check 

1474 :param resources: Dict containing Resource objects to validate against 

1475 """ 

1476 

1477 for name in resource_values.keys(): 

1478 _valid_method_call_check_resource(name, method, resources)