Skip to content

Commit 137575f

Browse files
Zuulopenstack-gerrit
authored andcommitted
Merge "Add logic to enforce local api and db limits"
2 parents 5dd515e + 3b69f95 commit 137575f

File tree

8 files changed

+393
-0
lines changed

8 files changed

+393
-0
lines changed

lower-constraints.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ oslo.context==3.4.0
7373
oslo.db==10.0.0
7474
oslo.i18n==5.1.0
7575
oslo.log==4.6.1
76+
oslo.limit==1.5.0
7677
oslo.messaging==10.3.0
7778
oslo.middleware==3.31.0
7879
oslo.policy==3.7.0

mypy-files.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
nova/compute/manager.py
22
nova/crypto.py
3+
nova/limit/local.py
34
nova/network/neutron.py
45
nova/pci
56
nova/privsep/path.py

nova/exception.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,10 @@ class GlanceConnectionFailed(NovaException):
135135
"%(reason)s")
136136

137137

138+
class KeystoneConnectionFailed(NovaException):
139+
msg_fmt = _("Connection to keystone host failed: %(reason)s")
140+
141+
138142
class CinderConnectionFailed(NovaException):
139143
msg_fmt = _("Connection to cinder host failed: %(reason)s")
140144

@@ -1281,6 +1285,14 @@ class PortLimitExceeded(OverQuota):
12811285
msg_fmt = _("Maximum number of ports exceeded")
12821286

12831287

1288+
class ServerGroupLimitExceeded(OverQuota):
1289+
msg_fmt = _("Quota exceeded, too many server groups.")
1290+
1291+
1292+
class GroupMemberLimitExceeded(OverQuota):
1293+
msg_fmt = _("Quota exceeded, too many servers in group")
1294+
1295+
12841296
class AggregateNotFound(NotFound):
12851297
msg_fmt = _("Aggregate %(aggregate_id)s could not be found.")
12861298

nova/limit/__init__.py

Whitespace-only changes.

nova/limit/local.py

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
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+
15+
import functools
16+
import typing as ty
17+
18+
from oslo_limit import exception as limit_exceptions
19+
from oslo_limit import limit
20+
from oslo_log import log as logging
21+
22+
import nova.conf
23+
from nova import context as nova_context
24+
from nova import exception
25+
from nova import objects
26+
27+
LOG = logging.getLogger(__name__)
28+
CONF = nova.conf.CONF
29+
30+
# Entity types for API Limits, same as names of config options prefixed with
31+
# "server_" to disambiguate them in keystone
32+
SERVER_METADATA_ITEMS = "server_metadata_items"
33+
INJECTED_FILES = "server_injected_files"
34+
INJECTED_FILES_CONTENT = "server_injected_file_content_bytes"
35+
INJECTED_FILES_PATH = "server_injected_file_path_bytes"
36+
API_LIMITS = set([
37+
SERVER_METADATA_ITEMS,
38+
INJECTED_FILES,
39+
INJECTED_FILES_CONTENT,
40+
INJECTED_FILES_PATH,
41+
])
42+
43+
# Entity types for all DB limits, same as names of config options prefixed with
44+
# "server_" to disambiguate them in keystone
45+
KEY_PAIRS = "server_key_pairs"
46+
SERVER_GROUPS = "server_groups"
47+
SERVER_GROUP_MEMBERS = "server_group_members"
48+
49+
# Checks only happen when we are using the unified limits driver
50+
UNIFIED_LIMITS_DRIVER = "nova.quota.UnifiedLimitsDriver"
51+
52+
# Map entity types to the exception we raise in the case that the resource is
53+
# over the allowed limit. Each of these should be a subclass of
54+
# exception.OverQuota.
55+
EXCEPTIONS = {
56+
KEY_PAIRS: exception.KeypairLimitExceeded,
57+
INJECTED_FILES_CONTENT: exception.OnsetFileContentLimitExceeded,
58+
INJECTED_FILES_PATH: exception.OnsetFilePathLimitExceeded,
59+
INJECTED_FILES: exception.OnsetFileLimitExceeded,
60+
SERVER_METADATA_ITEMS: exception.MetadataLimitExceeded,
61+
SERVER_GROUPS: exception.ServerGroupLimitExceeded,
62+
SERVER_GROUP_MEMBERS: exception.GroupMemberLimitExceeded,
63+
}
64+
65+
66+
def always_zero_usage(
67+
project_id: str, resource_names: ty.List[str]
68+
) -> ty.Dict[str, int]:
69+
"""Called by oslo_limit's enforcer"""
70+
# Return usage of 0 for API limits. Values in API requests will be used as
71+
# the deltas.
72+
return {resource_name: 0 for resource_name in resource_names}
73+
74+
75+
def enforce_api_limit(entity_type: str, count: int) -> None:
76+
"""Check if the values given are over the limit for that key.
77+
78+
This is generally used for limiting the size of certain API requests
79+
that eventually get stored in the database.
80+
"""
81+
if CONF.quota.driver != UNIFIED_LIMITS_DRIVER:
82+
return
83+
84+
if entity_type not in API_LIMITS:
85+
fmt = "%s is not a valid API limit: %s"
86+
raise ValueError(fmt % (entity_type, API_LIMITS))
87+
88+
try:
89+
enforcer = limit.Enforcer(always_zero_usage)
90+
except limit_exceptions.SessionInitError as e:
91+
msg = ("Failed to connect to keystone while enforcing %s quota limit."
92+
% entity_type)
93+
LOG.error(msg + " Error: " + str(e))
94+
raise exception.KeystoneConnectionFailed(msg)
95+
96+
try:
97+
enforcer.enforce(None, {entity_type: count})
98+
except limit_exceptions.ProjectOverLimit as e:
99+
# Copy the exception message to a OverQuota to propagate to the
100+
# API layer.
101+
raise EXCEPTIONS.get(entity_type, exception.OverQuota)(str(e))
102+
103+
104+
def enforce_db_limit(
105+
context: nova_context.RequestContext,
106+
entity_type: str,
107+
entity_scope: ty.Any,
108+
delta: int
109+
) -> None:
110+
"""Check provided delta does not put resource over limit.
111+
112+
Firstly we count the current usage given the specified scope.
113+
We then add that count to the specified delta to see if we
114+
are over the limit for that kind of entity.
115+
116+
Note previously we used to recheck these limits.
117+
However these are really soft DDoS protections,
118+
not hard resource limits, so we don't do the recheck for these.
119+
120+
The scope is specific to the limit type:
121+
* key_pairs scope is context.user_id
122+
* server_groups scope is context.project_id
123+
* server_group_members scope is server_group_uuid
124+
"""
125+
if CONF.quota.driver != UNIFIED_LIMITS_DRIVER:
126+
return
127+
128+
if entity_type not in DB_COUNT_FUNCTION.keys():
129+
fmt = "%s does not have a DB count function defined: %s"
130+
raise ValueError(fmt % (entity_type, DB_COUNT_FUNCTION.keys()))
131+
if delta < 0:
132+
raise ValueError("delta must be a positive integer")
133+
134+
count_function = DB_COUNT_FUNCTION[entity_type]
135+
136+
try:
137+
enforcer = limit.Enforcer(
138+
functools.partial(count_function, context, entity_scope))
139+
except limit_exceptions.SessionInitError as e:
140+
msg = ("Failed to connect to keystone while enforcing %s quota limit."
141+
% entity_type)
142+
LOG.error(msg + " Error: " + str(e))
143+
raise exception.KeystoneConnectionFailed(msg)
144+
145+
try:
146+
enforcer.enforce(None, {entity_type: delta})
147+
except limit_exceptions.ProjectOverLimit as e:
148+
# Copy the exception message to a OverQuota to propagate to the
149+
# API layer.
150+
raise EXCEPTIONS.get(entity_type, exception.OverQuota)(str(e))
151+
152+
153+
def _keypair_count(context, user_id, *args):
154+
count = objects.KeyPairList.get_count_by_user(context, user_id)
155+
return {'server_key_pairs': count}
156+
157+
158+
def _server_group_count(context, project_id, *args):
159+
raw_counts = objects.InstanceGroupList.get_counts(context, project_id)
160+
return {'server_groups': raw_counts['project']['server_groups']}
161+
162+
163+
def _server_group_members_count(context, server_group_uuid, *args):
164+
# NOTE(johngarbutt) we used to count members added per user
165+
server_group = objects.InstanceGroup.get_by_uuid(context,
166+
server_group_uuid)
167+
return {'server_group_members': len(server_group.members)}
168+
169+
170+
DB_COUNT_FUNCTION = {
171+
KEY_PAIRS: _keypair_count,
172+
SERVER_GROUPS: _server_group_count,
173+
SERVER_GROUP_MEMBERS: _server_group_members_count
174+
}

nova/tests/unit/limit/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)