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

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""" 

18WSGI middleware for OpenStack API controllers. 

19""" 

20import nova.monkey_patch # noqa 

21 

22from oslo_log import log as logging 

23import routes 

24import webob.dec 

25import webob.exc 

26 

27from nova.api.openstack import wsgi 

28from nova.api import wsgi as base_wsgi 

29import nova.conf 

30from nova.i18n import translate 

31 

32 

33LOG = logging.getLogger(__name__) 

34CONF = nova.conf.CONF 

35 

36 

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 

48 

49 

50class FaultWrapper(base_wsgi.Middleware): 

51 """Calls down the middleware stack, making exceptions into faults.""" 

52 

53 _status_to_type = {} 

54 

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)() 

62 

63 def _error(self, inner, req): 

64 LOG.exception("Caught error: %s", inner) 

65 

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 

71 

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) 

89 

90 return wsgi.Fault(outer) 

91 

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) 

98 

99 

100class LegacyV2CompatibleWrapper(base_wsgi.Middleware): 

101 

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 """ 

107 

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 

113 

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 """ 

118 

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] 

123 

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 

138 

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) 

145 

146 

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) 

153 

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) 

162 

163 

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-]+}' 

170 

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) 

188 

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) 

202 

203 def create_route(self, path, method, controller, action): 

204 project_id_token = self._get_project_id_token() 

205 

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) 

216 

217 

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)