Coverage for nova/api/wsgi.py: 89%

74 statements  

« prev     ^ index     » next       coverage.py v7.6.12, created at 2025-04-17 15:08 +0000

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

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

3# a copy of the License at 

4# 

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

6# 

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

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

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

10# License for the specific language governing permissions and limitations 

11# under the License. 

12"""WSGI primitives used throughout the nova WSGI apps.""" 

13 

14import os 

15 

16from oslo_log import log as logging 

17from paste import deploy 

18import routes.middleware 

19import webob 

20 

21import nova.conf 

22from nova import exception 

23from nova.i18n import _ 

24 

25 

26CONF = nova.conf.CONF 

27 

28LOG = logging.getLogger(__name__) 

29 

30 

31class Request(webob.Request): 

32 def __init__(self, environ, *args, **kwargs): 

33 if CONF.wsgi.secure_proxy_ssl_header: 

34 scheme = environ.get(CONF.wsgi.secure_proxy_ssl_header) 

35 if scheme: 

36 environ['wsgi.url_scheme'] = scheme 

37 super(Request, self).__init__(environ, *args, **kwargs) 

38 

39 

40class Application(object): 

41 """Base WSGI application wrapper. Subclasses need to implement __call__.""" 

42 

43 @classmethod 

44 def factory(cls, global_config, **local_config): 

45 """Used for paste app factories in paste.deploy config files. 

46 

47 Any local configuration (that is, values under the [app:APPNAME] 

48 section of the paste config) will be passed into the `__init__` method 

49 as kwargs. 

50 

51 A hypothetical configuration would look like: 

52 

53 [app:wadl] 

54 latest_version = 1.3 

55 paste.app_factory = nova.api.fancy_api:Wadl.factory 

56 

57 which would result in a call to the `Wadl` class as 

58 

59 import nova.api.fancy_api 

60 fancy_api.Wadl(latest_version='1.3') 

61 

62 You could of course re-implement the `factory` method in subclasses, 

63 but using the kwarg passing it shouldn't be necessary. 

64 

65 """ 

66 return cls(**local_config) 

67 

68 def __call__(self, environ, start_response): 

69 r"""Subclasses will probably want to implement __call__ like this: 

70 

71 @webob.dec.wsgify(RequestClass=Request) 

72 def __call__(self, req): 

73 # Any of the following objects work as responses: 

74 

75 # Option 1: simple string 

76 res = 'message\n' 

77 

78 # Option 2: a nicely formatted HTTP exception page 

79 res = exc.HTTPForbidden(explanation='Nice try') 

80 

81 # Option 3: a webob Response object (in case you need to play with 

82 # headers, or you want to be treated like an iterable, or ...) 

83 res = Response() 

84 res.app_iter = open('somefile') 

85 

86 # Option 4: any wsgi app to be run next 

87 res = self.application 

88 

89 # Option 5: you can get a Response object for a wsgi app, too, to 

90 # play with headers etc 

91 res = req.get_response(self.application) 

92 

93 # You can then just return your response... 

94 return res 

95 # ... or set req.response and return None. 

96 req.response = res 

97 

98 See the end of http://pythonpaste.org/webob/modules/dec.html 

99 for more info. 

100 

101 """ 

102 raise NotImplementedError(_('You must implement __call__')) 

103 

104 

105class Middleware(Application): 

106 """Base WSGI middleware. 

107 

108 These classes require an application to be 

109 initialized that will be called next. By default the middleware will 

110 simply call its wrapped app, or you can override __call__ to customize its 

111 behavior. 

112 

113 """ 

114 

115 @classmethod 

116 def factory(cls, global_config, **local_config): 

117 """Used for paste app factories in paste.deploy config files. 

118 

119 Any local configuration (that is, values under the [filter:APPNAME] 

120 section of the paste config) will be passed into the `__init__` method 

121 as kwargs. 

122 

123 A hypothetical configuration would look like: 

124 

125 [filter:analytics] 

126 redis_host = 127.0.0.1 

127 paste.filter_factory = nova.api.analytics:Analytics.factory 

128 

129 which would result in a call to the `Analytics` class as 

130 

131 import nova.api.analytics 

132 analytics.Analytics(app_from_paste, redis_host='127.0.0.1') 

133 

134 You could of course re-implement the `factory` method in subclasses, 

135 but using the kwarg passing it shouldn't be necessary. 

136 

137 """ 

138 def _factory(app): 

139 return cls(app, **local_config) 

140 return _factory 

141 

142 def __init__(self, application): 

143 self.application = application 

144 

145 def process_request(self, req): 

146 """Called on each request. 

147 

148 If this returns None, the next application down the stack will be 

149 executed. If it returns a response then that response will be returned 

150 and execution will stop here. 

151 

152 """ 

153 return None 

154 

155 def process_response(self, response): 

156 """Do whatever you'd like to the response.""" 

157 return response 

158 

159 @webob.dec.wsgify(RequestClass=Request) 

160 def __call__(self, req): 

161 response = self.process_request(req) 

162 if response: 

163 return response 

164 response = req.get_response(self.application) 

165 return self.process_response(response) 

166 

167 

168class Router(object): 

169 """WSGI middleware that maps incoming requests to WSGI apps.""" 

170 

171 def __init__(self, mapper): 

172 """Create a router for the given routes.Mapper. 

173 

174 Each route in `mapper` must specify a 'controller', which is a 

175 WSGI app to call. You'll probably want to specify an 'action' as 

176 well and have your controller be an object that can route 

177 the request to the action-specific method. 

178 

179 Examples: 

180 mapper = routes.Mapper() 

181 sc = ServerController() 

182 

183 # Explicit mapping of one route to a controller+action 

184 mapper.connect(None, '/svrlist', controller=sc, action='list') 

185 

186 # Actions are all implicitly defined 

187 mapper.resource('server', 'servers', controller=sc) 

188 

189 # Pointing to an arbitrary WSGI app. You can specify the 

190 # {path_info:.*} parameter so the target app can be handed just that 

191 # section of the URL. 

192 mapper.connect(None, '/v1.0/{path_info:.*}', controller=BlogApp()) 

193 

194 """ 

195 self.map = mapper 

196 self._router = routes.middleware.RoutesMiddleware(self._dispatch, 

197 self.map) 

198 

199 @webob.dec.wsgify(RequestClass=Request) 

200 def __call__(self, req): 

201 """Route the incoming request to a controller based on self.map. 

202 

203 If no match, return a 404. 

204 

205 """ 

206 return self._router 

207 

208 @staticmethod 

209 @webob.dec.wsgify(RequestClass=Request) 

210 def _dispatch(req): 

211 """Dispatch the request to the appropriate controller. 

212 

213 Called by self._router after matching the incoming request to a route 

214 and putting the information into req.environ. Either returns 404 

215 or the routed WSGI app's response. 

216 

217 """ 

218 match = req.environ['wsgiorg.routing_args'][1] 

219 if not match: 

220 return webob.exc.HTTPNotFound() 

221 app = match['controller'] 

222 return app 

223 

224 

225class Loader(object): 

226 """Used to load WSGI applications from paste configurations.""" 

227 

228 def __init__(self, config_path=None): 

229 """Initialize the loader, and attempt to find the config. 

230 

231 :param config_path: Full or relative path to the paste config. 

232 :returns: None 

233 

234 """ 

235 self.config_path = None 

236 

237 config_path = config_path or CONF.wsgi.api_paste_config 

238 if not os.path.isabs(config_path): 

239 self.config_path = CONF.find_file(config_path) 

240 elif os.path.exists(config_path): 

241 self.config_path = config_path 

242 

243 if not self.config_path: 

244 raise exception.ConfigNotFound(path=config_path) 

245 

246 def load_app(self, name): 

247 """Return the paste URLMap wrapped WSGI application. 

248 

249 :param name: Name of the application to load. 

250 :returns: Paste URLMap object wrapping the requested application. 

251 :raises: `nova.exception.PasteAppNotFound` 

252 

253 """ 

254 try: 

255 LOG.debug("Loading app %(name)s from %(path)s", 

256 {'name': name, 'path': self.config_path}) 

257 return deploy.loadapp("config:%s" % self.config_path, name=name) 

258 except LookupError: 

259 LOG.exception("Couldn't lookup app: %s", name) 

260 raise exception.PasteAppNotFound(name=name, path=self.config_path)