Coverage for nova/api/openstack/__init__.py: 86%
123 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 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.
17"""
18WSGI middleware for OpenStack API controllers.
19"""
20import nova.monkey_patch # noqa
22from oslo_log import log as logging
23import routes
24import webob.dec
25import webob.exc
27from nova.api.openstack import wsgi
28from nova.api import wsgi as base_wsgi
29import nova.conf
30from nova.i18n import translate
33LOG = logging.getLogger(__name__)
34CONF = nova.conf.CONF
37def walk_class_hierarchy(clazz, encountered=None):
38 """Walk class hierarchy, yielding most derived classes first."""
39 if not encountered:
40 encountered = []
41 for subclass in clazz.__subclasses__():
42 if subclass not in encountered: 42 ↛ 41line 42 didn't jump to line 41 because the condition on line 42 was always true
43 encountered.append(subclass)
44 # drill down to leaves first
45 for subsubclass in walk_class_hierarchy(subclass, encountered):
46 yield subsubclass
47 yield subclass
50class FaultWrapper(base_wsgi.Middleware):
51 """Calls down the middleware stack, making exceptions into faults."""
53 _status_to_type = {}
55 @staticmethod
56 def status_to_type(status):
57 if not FaultWrapper._status_to_type:
58 for clazz in walk_class_hierarchy(webob.exc.HTTPError):
59 FaultWrapper._status_to_type[clazz.code] = clazz
60 return FaultWrapper._status_to_type.get(
61 status, webob.exc.HTTPInternalServerError)()
63 def _error(self, inner, req):
64 LOG.exception("Caught error: %s", inner)
66 safe = getattr(inner, 'safe', False)
67 headers = getattr(inner, 'headers', None)
68 status = getattr(inner, 'code', 500)
69 if status is None:
70 status = 500
72 msg_dict = dict(url=req.url, status=status)
73 LOG.info("%(url)s returned with HTTP %(status)d", msg_dict)
74 outer = self.status_to_type(status)
75 if headers: 75 ↛ 76line 75 didn't jump to line 76 because the condition on line 75 was never true
76 outer.headers = headers
77 # NOTE(johannes): We leave the explanation empty here on
78 # purpose. It could possibly have sensitive information
79 # that should not be returned back to the user. See
80 # bugs 868360 and 874472
81 # NOTE(eglynn): However, it would be over-conservative and
82 # inconsistent with the EC2 API to hide every exception,
83 # including those that are safe to expose, see bug 1021373
84 if safe:
85 user_locale = req.best_match_language()
86 inner_msg = translate(inner.message, user_locale)
87 outer.explanation = '%s: %s' % (inner.__class__.__name__,
88 inner_msg)
90 return wsgi.Fault(outer)
92 @webob.dec.wsgify(RequestClass=wsgi.Request)
93 def __call__(self, req):
94 try:
95 return req.get_response(self.application)
96 except Exception as ex:
97 return self._error(ex, req)
100class LegacyV2CompatibleWrapper(base_wsgi.Middleware):
102 def _filter_request_headers(self, req):
103 """For keeping same behavior with v2 API, ignores microversions
104 HTTP headers X-OpenStack-Nova-API-Version and OpenStack-API-Version
105 in the request.
106 """
108 if wsgi.API_VERSION_REQUEST_HEADER in req.headers:
109 del req.headers[wsgi.API_VERSION_REQUEST_HEADER]
110 if wsgi.LEGACY_API_VERSION_REQUEST_HEADER in req.headers: 110 ↛ 111line 110 didn't jump to line 111 because the condition on line 110 was never true
111 del req.headers[wsgi.LEGACY_API_VERSION_REQUEST_HEADER]
112 return req
114 def _filter_response_headers(self, response):
115 """For keeping same behavior with v2 API, filter out microversions
116 HTTP header and microversions field in header 'Vary'.
117 """
119 if wsgi.API_VERSION_REQUEST_HEADER in response.headers:
120 del response.headers[wsgi.API_VERSION_REQUEST_HEADER]
121 if wsgi.LEGACY_API_VERSION_REQUEST_HEADER in response.headers:
122 del response.headers[wsgi.LEGACY_API_VERSION_REQUEST_HEADER]
124 if 'Vary' in response.headers:
125 vary_headers = response.headers['Vary'].split(',')
126 filtered_vary = []
127 for vary in vary_headers:
128 vary = vary.strip()
129 if (vary == wsgi.API_VERSION_REQUEST_HEADER or
130 vary == wsgi.LEGACY_API_VERSION_REQUEST_HEADER):
131 continue
132 filtered_vary.append(vary)
133 if filtered_vary:
134 response.headers['Vary'] = ','.join(filtered_vary)
135 else:
136 del response.headers['Vary']
137 return response
139 @webob.dec.wsgify(RequestClass=wsgi.Request)
140 def __call__(self, req):
141 req.set_legacy_v2()
142 req = self._filter_request_headers(req)
143 response = req.get_response(self.application)
144 return self._filter_response_headers(response)
147class APIMapper(routes.Mapper):
148 def routematch(self, url=None, environ=None):
149 if url == "": 149 ↛ 150line 149 didn't jump to line 150 because the condition on line 149 was never true
150 result = self._match("", environ)
151 return result[0], result[1]
152 return routes.Mapper.routematch(self, url, environ)
154 def connect(self, *args, **kargs):
155 # NOTE(vish): Default the format part of a route to only accept json
156 # and xml so it doesn't eat all characters after a '.'
157 # in the url.
158 kargs.setdefault('requirements', {})
159 if not kargs['requirements'].get('format'):
160 kargs['requirements']['format'] = 'json|xml'
161 return routes.Mapper.connect(self, *args, **kargs)
164class ProjectMapper(APIMapper):
165 def _get_project_id_token(self):
166 # NOTE(sdague): project_id parameter is only valid if its hex
167 # or hex + dashes (note, integers are a subset of this). This
168 # is required to hand our overlapping routes issues.
169 return '{project_id:[0-9a-f-]+}'
171 def resource(self, member_name, collection_name, **kwargs):
172 project_id_token = self._get_project_id_token()
173 if 'parent_resource' not in kwargs: 173 ↛ 176line 173 didn't jump to line 176 because the condition on line 173 was always true
174 kwargs['path_prefix'] = '%s/' % project_id_token
175 else:
176 parent_resource = kwargs['parent_resource']
177 p_collection = parent_resource['collection_name']
178 p_member = parent_resource['member_name']
179 kwargs['path_prefix'] = '%s/%s/:%s_id' % (
180 project_id_token,
181 p_collection,
182 p_member)
183 routes.Mapper.resource(
184 self,
185 member_name,
186 collection_name,
187 **kwargs)
189 # while we are in transition mode, create additional routes
190 # for the resource that do not include project_id.
191 if 'parent_resource' not in kwargs: 191 ↛ 194line 191 didn't jump to line 194 because the condition on line 191 was always true
192 del kwargs['path_prefix']
193 else:
194 parent_resource = kwargs['parent_resource']
195 p_collection = parent_resource['collection_name']
196 p_member = parent_resource['member_name']
197 kwargs['path_prefix'] = '%s/:%s_id' % (p_collection,
198 p_member)
199 routes.Mapper.resource(self, member_name,
200 collection_name,
201 **kwargs)
203 def create_route(self, path, method, controller, action):
204 project_id_token = self._get_project_id_token()
206 # while we transition away from project IDs in the API URIs, create
207 # additional routes that include the project_id
208 self.connect('/%s%s' % (project_id_token, path),
209 conditions=dict(method=[method]),
210 controller=controller,
211 action=action)
212 self.connect(path,
213 conditions=dict(method=[method]),
214 controller=controller,
215 action=action)
218class PlainMapper(APIMapper):
219 def resource(self, member_name, collection_name, **kwargs):
220 if 'parent_resource' in kwargs: 220 ↛ 221line 220 didn't jump to line 221 because the condition on line 220 was never true
221 parent_resource = kwargs['parent_resource']
222 p_collection = parent_resource['collection_name']
223 p_member = parent_resource['member_name']
224 kwargs['path_prefix'] = '%s/:%s_id' % (p_collection, p_member)
225 routes.Mapper.resource(self, member_name,
226 collection_name,
227 **kwargs)