Coverage for nova/limit/utils.py: 66%
67 statements
« prev ^ index » next coverage.py v7.6.12, created at 2025-04-17 15:08 +0000
« prev ^ index » next coverage.py v7.6.12, created at 2025-04-17 15:08 +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.
15import typing as ty
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
20from oslo_limit import exception as limit_exceptions
21from oslo_log import log as logging
23import nova.conf
24from nova import utils as nova_utils
26LOG = logging.getLogger(__name__)
27CONF = nova.conf.CONF
29UNIFIED_LIMITS_DRIVER = "nova.quota.UnifiedLimitsDriver"
30IDENTITY_CLIENT = None
33def use_unified_limits():
34 return CONF.quota.driver == UNIFIED_LIMITS_DRIVER
37class IdentityClient:
38 connection: 'proxy.Proxy'
39 service_id: str
40 region_id: str
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
47 def registered_limits(self):
48 return list(self.connection.registered_limits(
49 service_id=self.service_id, region_id=self.region_id))
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
91def should_enforce(exc: limit_exceptions.ProjectOverLimit) -> bool:
92 """Whether the exceeded resource limit should be enforced.
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.
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
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
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}
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()
147 # Make a set of resource names of the registered limits.
148 have_limits_set = {limit.resource_name for limit in registered_limits}
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
155 # The resource list will be either a require list or an ignore list.
156 require_or_ignore = CONF.quota.unified_limits_resource_list
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)