Coverage for nova/limit/utils.py: 66%

67 statements  

« prev     ^ index     » next       coverage.py v7.6.12, created at 2025-04-24 11:16 +0000

1# Copyright 2022 StackHPC 

2# 

3# Licensed under the Apache License, Version 2.0 (the "License"); you may 

4# not use this file except in compliance with the License. You may obtain 

5# a copy of the License at 

6# 

7# http://www.apache.org/licenses/LICENSE-2.0 

8# 

9# Unless required by applicable law or agreed to in writing, software 

10# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 

11# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 

12# License for the specific language governing permissions and limitations 

13# under the License. 

14 

15import typing as ty 

16 

17if ty.TYPE_CHECKING: 17 ↛ 18line 17 didn't jump to line 18 because the condition on line 17 was never true

18 from openstack import proxy 

19 

20from oslo_limit import exception as limit_exceptions 

21from oslo_log import log as logging 

22 

23import nova.conf 

24from nova import utils as nova_utils 

25 

26LOG = logging.getLogger(__name__) 

27CONF = nova.conf.CONF 

28 

29UNIFIED_LIMITS_DRIVER = "nova.quota.UnifiedLimitsDriver" 

30IDENTITY_CLIENT = None 

31 

32 

33def use_unified_limits(): 

34 return CONF.quota.driver == UNIFIED_LIMITS_DRIVER 

35 

36 

37class IdentityClient: 

38 connection: 'proxy.Proxy' 

39 service_id: str 

40 region_id: str 

41 

42 def __init__(self, connection, service_id, region_id): 

43 self.connection = connection 

44 self.service_id = service_id 

45 self.region_id = region_id 

46 

47 def registered_limits(self): 

48 return list(self.connection.registered_limits( 

49 service_id=self.service_id, region_id=self.region_id)) 

50 

51 

52def _identity_client(): 

53 global IDENTITY_CLIENT 

54 if not IDENTITY_CLIENT: 54 ↛ 88line 54 didn't jump to line 88 because the condition on line 54 was always true

55 connection = nova_utils.get_sdk_adapter( 

56 'identity', True, conf_group='oslo_limit') 

57 service_id = None 

58 region_id = None 

59 # Prefer the endpoint_id if present, same as oslo.limit. 

60 if CONF.oslo_limit.endpoint_id is not None: 60 ↛ 64line 60 didn't jump to line 64 because the condition on line 60 was always true

61 endpoint = connection.get_endpoint(CONF.oslo_limit.endpoint_id) 

62 service_id = endpoint.service_id 

63 region_id = endpoint.region_id 

64 elif 'endpoint_service_type' in CONF.oslo_limit: 

65 # This must be oslo.limit >= 2.6.0 and this block is more or less 

66 # copied from there. 

67 if (not CONF.oslo_limit.endpoint_service_type and not 

68 CONF.oslo_limit.endpoint_service_name): 

69 raise ValueError( 

70 'Either endpoint_service_type or endpoint_service_name ' 

71 'must be set') 

72 # Get the service_id for registered limits calls. 

73 services = connection.services( 

74 type=CONF.oslo_limit.endpoint_service_type, 

75 name=CONF.oslo_limit.endpoint_service_name) 

76 if len(services) > 1: 

77 raise ValueError('Multiple services found') 

78 service_id = services[0].id 

79 # Get the region_id if region name is configured. 

80 # endpoint_region_name was added in oslo.limit 2.6.0. 

81 if CONF.oslo_limit.endpoint_region_name: 

82 regions = connection.regions( 

83 name=CONF.oslo_limit.endpoint_region_name) 

84 if len(regions) > 1: 

85 raise ValueError('Multiple regions found') 

86 region_id = regions[0].id 

87 IDENTITY_CLIENT = IdentityClient(connection, service_id, region_id) 

88 return IDENTITY_CLIENT 

89 

90 

91def should_enforce(exc: limit_exceptions.ProjectOverLimit) -> bool: 

92 """Whether the exceeded resource limit should be enforced. 

93 

94 Given a ProjectOverLimit exception from oslo.limit, check whether the 

95 involved limit(s) should be enforced. This is needed if we need more logic 

96 than is available by default in oslo.limit. 

97 

98 :param exc: An oslo.limit ProjectOverLimit exception instance, which 

99 contains a list of OverLimitInfo. Each OverLimitInfo includes a 

100 resource_name, limit, current_usage, and delta. 

101 """ 

102 # If any exceeded limit is greater than zero, it means an explicitly set 

103 # limit has been enforced. And if any explicitly set limit has gone over 

104 # quota, the enforcement should be upheld and there is no need to consider 

105 # the potential for unset limits. 

106 if any(info.limit > 0 for info in exc.over_limit_info_list): 

107 return True 

108 

109 # Next, if all of the exceeded limits are -1, we don't need to enforce and 

110 # we can avoid calling Keystone for the list of registered limits. 

111 # 

112 # A value of -1 is documented in Keystone as meaning unlimited: 

113 # 

114 # "Note 

115 # The default limit of registered limit and the resource limit of project 

116 # limit now are limited from -1 to 2147483647 (integer). -1 means no limit 

117 # and 2147483647 is the max value for user to define limits." 

118 # 

119 # https://docs.openstack.org/keystone/latest/admin/unified-limits.html#what-is-a-limit 

120 # 

121 # but oslo.limit enforce does not treat -1 as unlimited at this time and 

122 # instead uses its literal integer value. We will consider any negative 

123 # limit value as unlimited. 

124 if all(info.limit < 0 for info in exc.over_limit_info_list): 124 ↛ 125line 124 didn't jump to line 125 because the condition on line 124 was never true

125 return False 

126 

127 # Only resources with exceeded limits of "0" are candidates for 

128 # enforcement. 

129 # 

130 # A limit of "0" in the over_limit_info_list means that oslo.limit is 

131 # telling us the limit is 0. But oslo.limit returns 0 for two cases: 

132 # a) it found a limit of 0 in Keystone or b) it did not find a limit in 

133 # Keystone at all. 

134 # 

135 # We will need to query the list of registered limits from Keystone in 

136 # order to determine whether each "0" limit is case a) or case b). 

137 enforce_candidates = { 

138 info.resource_name for info in exc.over_limit_info_list 

139 if info.limit == 0} 

140 

141 # Get a list of all the registered limits. There is not a way to filter by 

142 # resource names however this will do one API call whereas the alternative 

143 # is calling GET /registered_limits/{registered_limit_id} for each resource 

144 # name. 

145 registered_limits = _identity_client().registered_limits() 

146 

147 # Make a set of resource names of the registered limits. 

148 have_limits_set = {limit.resource_name for limit in registered_limits} 

149 

150 # If any candidates have limits set, enforce. It means at least one limit 

151 # has been explicitly set to 0. 

152 if enforce_candidates & have_limits_set: 

153 return True 

154 

155 # The resource list will be either a require list or an ignore list. 

156 require_or_ignore = CONF.quota.unified_limits_resource_list 

157 

158 strategy = CONF.quota.unified_limits_resource_strategy 

159 enforced = enforce_candidates 

160 if strategy == 'require': 160 ↛ 163line 160 didn't jump to line 163 because the condition on line 160 was never true

161 # Resources that are in both the candidate list and in the require list 

162 # should be enforced. 

163 enforced = enforce_candidates & set(require_or_ignore) 

164 elif strategy == 'ignore': 164 ↛ 169line 164 didn't jump to line 169 because the condition on line 164 was always true

165 # Resources that are in the candidate list but are not in the ignore 

166 # list should be enforced. 

167 enforced = enforce_candidates - set(require_or_ignore) 

168 else: 

169 LOG.error( 

170 f'Invalid strategy value: {strategy} is specified in the ' 

171 '[quota]unified_limits_resource_strategy config option, so ' 

172 f'enforcing for resources {enforced}') 

173 # Log in case we need to debug unexpected enforcement or non-enforcement. 

174 msg = ( 

175 f'enforcing for resources {enforced}' if enforced else 'not enforcing') 

176 LOG.debug( 

177 f'Resources {enforce_candidates} have no registered limits set in ' 

178 f'Keystone. [quota]unified_limits_resource_strategy is {strategy} and ' 

179 f'[quota]unified_limits_resource_list is {require_or_ignore}, ' 

180 f'so {msg}') 

181 return bool(enforced)