~salvatore-orlando/neutron/bug814518

« back to all changes in this revision

Viewing changes to quantum/common/wsgi.py

* Merged changes from Salvatore's branch - quantum-api-workinprogress
* Removed spurious methods from quantum_base_plugin class.
* Updated the sample plugins to be compliant with the new QuantumBase class.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
 
1
2
# vim: tabstop=4 shiftwidth=4 softtabstop=4
2
3
#
3
4
# Copyright 2011, Nicira Networks, Inc.
24
25
import sys
25
26
import datetime
26
27
 
 
28
from xml.dom import minidom
 
29
 
27
30
import eventlet
28
31
import eventlet.wsgi
29
32
eventlet.patcher.monkey_patch(all=False, socket=True)
32
35
import webob.dec
33
36
import webob.exc
34
37
 
 
38
from quantum import utils
 
39
from quantum.common import exceptions as exception
 
40
 
 
41
LOG = logging.getLogger('quantum.common.wsgi')
35
42
 
36
43
class WritableLogger(object):
37
44
    """A thin wrapper that responds to `write` and logs."""
110
117
        return self.process_response(response)
111
118
 
112
119
 
 
120
class Request(webob.Request):
 
121
 
 
122
    def best_match_content_type(self):
 
123
        """Determine the most acceptable content-type.
 
124
 
 
125
        Based on the query extension then the Accept header.
 
126
 
 
127
        """
 
128
        parts = self.path.rsplit('.', 1)
 
129
        LOG.debug("Request parts:%s",parts)
 
130
        if len(parts) > 1:
 
131
            format = parts[1]
 
132
            if format in ['json', 'xml']:
 
133
                return 'application/{0}'.format(parts[1])
 
134
 
 
135
        ctypes = ['application/json', 'application/xml']
 
136
        bm = self.accept.best_match(ctypes)
 
137
        LOG.debug("BM:%s",bm)
 
138
        return bm or 'application/json'
 
139
 
 
140
    def get_content_type(self):
 
141
        allowed_types = ("application/xml", "application/json")
 
142
        if not "Content-Type" in self.headers:
 
143
            msg = _("Missing Content-Type")
 
144
            LOG.debug(msg)
 
145
            raise webob.exc.HTTPBadRequest(msg)
 
146
        type = self.content_type
 
147
        if type in allowed_types:
 
148
            return type
 
149
        LOG.debug(_("Wrong Content-Type: %s") % type)
 
150
        raise webob.exc.HTTPBadRequest("Invalid content type")
 
151
 
 
152
 
 
153
class Application(object):
 
154
    """Base WSGI application wrapper. Subclasses need to implement __call__."""
 
155
 
 
156
    @classmethod
 
157
    def factory(cls, global_config, **local_config):
 
158
        """Used for paste app factories in paste.deploy config files.
 
159
 
 
160
        Any local configuration (that is, values under the [app:APPNAME]
 
161
        section of the paste config) will be passed into the `__init__` method
 
162
        as kwargs.
 
163
 
 
164
        A hypothetical configuration would look like:
 
165
 
 
166
            [app:wadl]
 
167
            latest_version = 1.3
 
168
            paste.app_factory = nova.api.fancy_api:Wadl.factory
 
169
 
 
170
        which would result in a call to the `Wadl` class as
 
171
 
 
172
            import quantum.api.fancy_api
 
173
            fancy_api.Wadl(latest_version='1.3')
 
174
 
 
175
        You could of course re-implement the `factory` method in subclasses,
 
176
        but using the kwarg passing it shouldn't be necessary.
 
177
 
 
178
        """
 
179
        return cls(**local_config)
 
180
 
 
181
    def __call__(self, environ, start_response):
 
182
        r"""Subclasses will probably want to implement __call__ like this:
 
183
 
 
184
        @webob.dec.wsgify(RequestClass=Request)
 
185
        def __call__(self, req):
 
186
          # Any of the following objects work as responses:
 
187
 
 
188
          # Option 1: simple string
 
189
          res = 'message\n'
 
190
 
 
191
          # Option 2: a nicely formatted HTTP exception page
 
192
          res = exc.HTTPForbidden(detail='Nice try')
 
193
 
 
194
          # Option 3: a webob Response object (in case you need to play with
 
195
          # headers, or you want to be treated like an iterable, or or or)
 
196
          res = Response();
 
197
          res.app_iter = open('somefile')
 
198
 
 
199
          # Option 4: any wsgi app to be run next
 
200
          res = self.application
 
201
 
 
202
          # Option 5: you can get a Response object for a wsgi app, too, to
 
203
          # play with headers etc
 
204
          res = req.get_response(self.application)
 
205
 
 
206
          # You can then just return your response...
 
207
          return res
 
208
          # ... or set req.response and return None.
 
209
          req.response = res
 
210
 
 
211
        See the end of http://pythonpaste.org/webob/modules/dec.html
 
212
        for more info.
 
213
 
 
214
        """
 
215
        raise NotImplementedError(_('You must implement __call__'))
 
216
 
 
217
 
113
218
class Debug(Middleware):
114
219
    """
115
220
    Helper class that can be inserted into any WSGI application chain
152
257
    WSGI middleware that maps incoming requests to WSGI apps.
153
258
    """
154
259
 
 
260
    @classmethod
 
261
    def factory(cls, global_config, **local_config):
 
262
        """
 
263
        Returns an instance of the WSGI Router class
 
264
        """
 
265
        return cls()
 
266
 
155
267
    def __init__(self, mapper):
156
268
        """
157
269
        Create a router for the given routes.Mapper.
169
281
          mapper.connect(None, "/svrlist", controller=sc, action="list")
170
282
 
171
283
          # Actions are all implicitly defined
172
 
          mapper.resource("server", "servers", controller=sc)
 
284
          mapper.resource("network", "networks", controller=nc)
173
285
 
174
286
          # Pointing to an arbitrary WSGI app.  You can specify the
175
287
          # {path_info:.*} parameter so the target app can be handed just that
186
298
        Route the incoming request to a controller based on self.map.
187
299
        If no match, return a 404.
188
300
        """
 
301
        LOG.debug("HERE - wsgi.Router.__call__")
189
302
        return self._router
190
303
 
191
304
    @staticmethod
204
317
 
205
318
 
206
319
class Controller(object):
207
 
    """
 
320
    """WSGI app that dispatched to methods.
 
321
 
208
322
    WSGI app that reads routing information supplied by RoutesMiddleware
209
323
    and calls the requested action method upon itself.  All action methods
210
324
    must, in addition to their normal parameters, accept a 'req' argument
211
 
    which is the incoming webob.Request.  They raise a webob.exc exception,
 
325
    which is the incoming wsgi.Request.  They raise a webob.exc exception,
212
326
    or return a dict which will be serialized by requested content type.
 
327
 
213
328
    """
214
329
 
215
 
    @webob.dec.wsgify
 
330
    @webob.dec.wsgify(RequestClass=Request)
216
331
    def __call__(self, req):
217
332
        """
218
333
        Call the method specified in req.environ by RoutesMiddleware.
219
334
        """
 
335
        LOG.debug("HERE - wsgi.Controller.__call__")
220
336
        arg_dict = req.environ['wsgiorg.routing_args'][1]
221
337
        action = arg_dict['action']
222
338
        method = getattr(self, action)
 
339
        LOG.debug("ARG_DICT:%s",arg_dict)
 
340
        LOG.debug("Action:%s",action)
 
341
        LOG.debug("Method:%s",method)
 
342
        LOG.debug("%s %s" % (req.method, req.url))
223
343
        del arg_dict['controller']
224
344
        del arg_dict['action']
225
 
        arg_dict['request'] = req
 
345
        if 'format' in arg_dict:
 
346
            del arg_dict['format']
 
347
        arg_dict['req'] = req
226
348
        result = method(**arg_dict)
 
349
 
227
350
        if type(result) is dict:
228
 
            return self._serialize(result, req)
 
351
            content_type = req.best_match_content_type()
 
352
            LOG.debug("Content type:%s",content_type)
 
353
            LOG.debug("Result:%s",result)
 
354
            default_xmlns = self.get_default_xmlns(req)
 
355
            body = self._serialize(result, content_type, default_xmlns)
 
356
 
 
357
            response = webob.Response()
 
358
            response.headers['Content-Type'] = content_type
 
359
            response.body = body
 
360
            msg_dict = dict(url=req.url, status=response.status_int)
 
361
            msg = _("%(url)s returned with HTTP %(status)d") % msg_dict
 
362
            LOG.debug(msg)
 
363
            return response
229
364
        else:
230
365
            return result
231
366
 
232
 
    def _serialize(self, data, request):
233
 
        """
234
 
        Serialize the given dict to the response type requested in request.
235
 
        Uses self._serialization_metadata if it exists, which is a dict mapping
236
 
        MIME types to information needed to serialize to that type.
237
 
        """
238
 
        _metadata = getattr(type(self), "_serialization_metadata", {})
239
 
        serializer = Serializer(request.environ, _metadata)
240
 
        return serializer.to_content_type(data)
 
367
    def _serialize(self, data, content_type, default_xmlns):
 
368
        """Serialize the given dict to the provided content_type.
 
369
 
 
370
        Uses self._serialization_metadata if it exists, which is a dict mapping
 
371
        MIME types to information needed to serialize to that type.
 
372
 
 
373
        """
 
374
        _metadata = getattr(type(self), '_serialization_metadata', {})
 
375
 
 
376
        serializer = Serializer(_metadata, default_xmlns)
 
377
        try:
 
378
            return serializer.serialize(data, content_type)
 
379
        except exception.InvalidContentType:
 
380
            raise webob.exc.HTTPNotAcceptable()
 
381
 
 
382
    def _deserialize(self, data, content_type):
 
383
        """Deserialize the request body to the specefied content type.
 
384
 
 
385
        Uses self._serialization_metadata if it exists, which is a dict mapping
 
386
        MIME types to information needed to serialize to that type.
 
387
 
 
388
        """
 
389
        _metadata = getattr(type(self), '_serialization_metadata', {})
 
390
        serializer = Serializer(_metadata)
 
391
        return serializer.deserialize(data, content_type)
 
392
 
 
393
    def get_default_xmlns(self, req):
 
394
        """Provide the XML namespace to use if none is otherwise specified."""
 
395
        return None
241
396
 
242
397
 
243
398
class Serializer(object):
244
 
    """
245
 
    Serializes a dictionary to a Content Type specified by a WSGI environment.
246
 
    """
247
 
 
248
 
    def __init__(self, environ, metadata=None):
249
 
        """
250
 
        Create a serializer based on the given WSGI environment.
 
399
    """Serializes and deserializes dictionaries to certain MIME types."""
 
400
 
 
401
    def __init__(self, metadata=None, default_xmlns=None):
 
402
        """Create a serializer based on the given WSGI environment.
 
403
 
251
404
        'metadata' is an optional dict mapping MIME types to information
252
405
        needed to serialize a dictionary to that type.
 
406
 
253
407
        """
254
 
        self.environ = environ
255
408
        self.metadata = metadata or {}
256
 
        self._methods = {
 
409
        self.default_xmlns = default_xmlns
 
410
 
 
411
    def _get_serialize_handler(self, content_type):
 
412
        handlers = {
257
413
            'application/json': self._to_json,
258
 
            'application/xml': self._to_xml}
259
 
 
260
 
    def to_content_type(self, data):
261
 
        """
262
 
        Serialize a dictionary into a string.  The format of the string
263
 
        will be decided based on the Content Type requested in self.environ:
264
 
        by Accept: header, or by URL suffix.
265
 
        """
266
 
        # FIXME(sirp): for now, supporting json only
267
 
        #mimetype = 'application/xml'
268
 
        mimetype = 'application/json'
269
 
        # TODO(gundlach): determine mimetype from request
270
 
        return self._methods.get(mimetype, repr)(data)
 
414
            'application/xml': self._to_xml,
 
415
        }
 
416
 
 
417
        try:
 
418
            return handlers[content_type]
 
419
        except Exception:
 
420
            raise exception.InvalidContentType(content_type=content_type)
 
421
 
 
422
    def serialize(self, data, content_type):
 
423
        """Serialize a dictionary into the specified content type."""
 
424
        return self._get_serialize_handler(content_type)(data)
 
425
 
 
426
    def deserialize(self, datastring, content_type):
 
427
        """Deserialize a string to a dictionary.
 
428
 
 
429
        The string must be in the format of a supported MIME type.
 
430
 
 
431
        """
 
432
        return self.get_deserialize_handler(content_type)(datastring)
 
433
 
 
434
    def get_deserialize_handler(self, content_type):
 
435
        handlers = {
 
436
            'application/json': self._from_json,
 
437
            'application/xml': self._from_xml,
 
438
        }
 
439
 
 
440
        try:
 
441
            return handlers[content_type]
 
442
        except Exception:
 
443
            raise exception.InvalidContentType(content_type=content_type)
 
444
 
 
445
    def _from_json(self, datastring):
 
446
        return utils.loads(datastring)
 
447
 
 
448
    def _from_xml(self, datastring):
 
449
        xmldata = self.metadata.get('application/xml', {})
 
450
        plurals = set(xmldata.get('plurals', {}))
 
451
        node = minidom.parseString(datastring).childNodes[0]
 
452
        return {node.nodeName: self._from_xml_node(node, plurals)}
 
453
 
 
454
    def _from_xml_node(self, node, listnames):
 
455
        """Convert a minidom node to a simple Python type.
 
456
 
 
457
        listnames is a collection of names of XML nodes whose subnodes should
 
458
        be considered list items.
 
459
 
 
460
        """
 
461
        if len(node.childNodes) == 1 and node.childNodes[0].nodeType == 3:
 
462
            return node.childNodes[0].nodeValue
 
463
        elif node.nodeName in listnames:
 
464
            return [self._from_xml_node(n, listnames) for n in node.childNodes]
 
465
        else:
 
466
            result = dict()
 
467
            for attr in node.attributes.keys():
 
468
                result[attr] = node.attributes[attr].nodeValue
 
469
            for child in node.childNodes:
 
470
                if child.nodeType != node.TEXT_NODE:
 
471
                    result[child.nodeName] = self._from_xml_node(child,
 
472
                                                                 listnames)
 
473
            return result
271
474
 
272
475
    def _to_json(self, data):
273
 
        def sanitizer(obj):
274
 
            if isinstance(obj, datetime.datetime):
275
 
                return obj.isoformat()
276
 
            return obj
277
 
 
278
 
        return json.dumps(data, default=sanitizer)
 
476
        return utils.dumps(data)
279
477
 
280
478
    def _to_xml(self, data):
281
479
        metadata = self.metadata.get('application/xml', {})
282
480
        # We expect data to contain a single key which is the XML root.
283
481
        root_key = data.keys()[0]
284
 
        from xml.dom import minidom
285
482
        doc = minidom.Document()
286
483
        node = self._to_xml_node(doc, metadata, root_key, data[root_key])
 
484
 
 
485
        xmlns = node.getAttribute('xmlns')
 
486
        if not xmlns and self.default_xmlns:
 
487
            node.setAttribute('xmlns', self.default_xmlns)
 
488
 
287
489
        return node.toprettyxml(indent='    ')
288
490
 
289
491
    def _to_xml_node(self, doc, metadata, nodename, data):
290
492
        """Recursive method to convert data members to XML nodes."""
291
493
        result = doc.createElement(nodename)
 
494
 
 
495
        # Set the xml namespace if one is specified
 
496
        # TODO(justinsb): We could also use prefixes on the keys
 
497
        xmlns = metadata.get('xmlns', None)
 
498
        if xmlns:
 
499
            result.setAttribute('xmlns', xmlns)
 
500
        LOG.debug("DATA:%s",data)
292
501
        if type(data) is list:
 
502
            LOG.debug("TYPE IS LIST")
 
503
            collections = metadata.get('list_collections', {})
 
504
            if nodename in collections:
 
505
                metadata = collections[nodename]
 
506
                for item in data:
 
507
                    node = doc.createElement(metadata['item_name'])
 
508
                    node.setAttribute(metadata['item_key'], str(item))
 
509
                    result.appendChild(node)
 
510
                return result
293
511
            singular = metadata.get('plurals', {}).get(nodename, None)
294
512
            if singular is None:
295
513
                if nodename.endswith('s'):
300
518
                node = self._to_xml_node(doc, metadata, singular, item)
301
519
                result.appendChild(node)
302
520
        elif type(data) is dict:
 
521
            LOG.debug("TYPE IS DICT")
 
522
            collections = metadata.get('dict_collections', {})
 
523
            if nodename in collections:
 
524
                metadata = collections[nodename]
 
525
                for k, v in data.items():
 
526
                    node = doc.createElement(metadata['item_name'])
 
527
                    node.setAttribute(metadata['item_key'], str(k))
 
528
                    text = doc.createTextNode(str(v))
 
529
                    node.appendChild(text)
 
530
                    result.appendChild(node)
 
531
                return result
303
532
            attrs = metadata.get('attributes', {}).get(nodename, {})
304
533
            for k, v in data.items():
305
534
                if k in attrs:
307
536
                else:
308
537
                    node = self._to_xml_node(doc, metadata, k, v)
309
538
                    result.appendChild(node)
310
 
        else:  # atom
 
539
        else:
 
540
            # Type is atom
 
541
            LOG.debug("TYPE IS ATOM:%s",data)
311
542
            node = doc.createTextNode(str(data))
312
543
            result.appendChild(node)
313
544
        return result
 
545