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
« 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."""
14import os
16from oslo_log import log as logging
17from paste import deploy
18import routes.middleware
19import webob
21import nova.conf
22from nova import exception
23from nova.i18n import _
26CONF = nova.conf.CONF
28LOG = logging.getLogger(__name__)
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)
40class Application(object):
41 """Base WSGI application wrapper. Subclasses need to implement __call__."""
43 @classmethod
44 def factory(cls, global_config, **local_config):
45 """Used for paste app factories in paste.deploy config files.
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.
51 A hypothetical configuration would look like:
53 [app:wadl]
54 latest_version = 1.3
55 paste.app_factory = nova.api.fancy_api:Wadl.factory
57 which would result in a call to the `Wadl` class as
59 import nova.api.fancy_api
60 fancy_api.Wadl(latest_version='1.3')
62 You could of course re-implement the `factory` method in subclasses,
63 but using the kwarg passing it shouldn't be necessary.
65 """
66 return cls(**local_config)
68 def __call__(self, environ, start_response):
69 r"""Subclasses will probably want to implement __call__ like this:
71 @webob.dec.wsgify(RequestClass=Request)
72 def __call__(self, req):
73 # Any of the following objects work as responses:
75 # Option 1: simple string
76 res = 'message\n'
78 # Option 2: a nicely formatted HTTP exception page
79 res = exc.HTTPForbidden(explanation='Nice try')
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')
86 # Option 4: any wsgi app to be run next
87 res = self.application
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)
93 # You can then just return your response...
94 return res
95 # ... or set req.response and return None.
96 req.response = res
98 See the end of http://pythonpaste.org/webob/modules/dec.html
99 for more info.
101 """
102 raise NotImplementedError(_('You must implement __call__'))
105class Middleware(Application):
106 """Base WSGI middleware.
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.
113 """
115 @classmethod
116 def factory(cls, global_config, **local_config):
117 """Used for paste app factories in paste.deploy config files.
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.
123 A hypothetical configuration would look like:
125 [filter:analytics]
126 redis_host = 127.0.0.1
127 paste.filter_factory = nova.api.analytics:Analytics.factory
129 which would result in a call to the `Analytics` class as
131 import nova.api.analytics
132 analytics.Analytics(app_from_paste, redis_host='127.0.0.1')
134 You could of course re-implement the `factory` method in subclasses,
135 but using the kwarg passing it shouldn't be necessary.
137 """
138 def _factory(app):
139 return cls(app, **local_config)
140 return _factory
142 def __init__(self, application):
143 self.application = application
145 def process_request(self, req):
146 """Called on each request.
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.
152 """
153 return None
155 def process_response(self, response):
156 """Do whatever you'd like to the response."""
157 return response
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)
168class Router(object):
169 """WSGI middleware that maps incoming requests to WSGI apps."""
171 def __init__(self, mapper):
172 """Create a router for the given routes.Mapper.
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.
179 Examples:
180 mapper = routes.Mapper()
181 sc = ServerController()
183 # Explicit mapping of one route to a controller+action
184 mapper.connect(None, '/svrlist', controller=sc, action='list')
186 # Actions are all implicitly defined
187 mapper.resource('server', 'servers', controller=sc)
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())
194 """
195 self.map = mapper
196 self._router = routes.middleware.RoutesMiddleware(self._dispatch,
197 self.map)
199 @webob.dec.wsgify(RequestClass=Request)
200 def __call__(self, req):
201 """Route the incoming request to a controller based on self.map.
203 If no match, return a 404.
205 """
206 return self._router
208 @staticmethod
209 @webob.dec.wsgify(RequestClass=Request)
210 def _dispatch(req):
211 """Dispatch the request to the appropriate controller.
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.
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
225class Loader(object):
226 """Used to load WSGI applications from paste configurations."""
228 def __init__(self, config_path=None):
229 """Initialize the loader, and attempt to find the config.
231 :param config_path: Full or relative path to the paste config.
232 :returns: None
234 """
235 self.config_path = None
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
243 if not self.config_path:
244 raise exception.ConfigNotFound(path=config_path)
246 def load_app(self, name):
247 """Return the paste URLMap wrapped WSGI application.
249 :param name: Name of the application to load.
250 :returns: Paste URLMap object wrapping the requested application.
251 :raises: `nova.exception.PasteAppNotFound`
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)