15. OpenStack REST-API基础:paste/webob/routes库

Tip

在OpenStack 组件中,每一类组件服务入口都是通过rest-API提供的。而python rest-api服务的发布涉及 到wsgi,路由分发等诸多问题。openstack使用的paste+webob+routes模块,使用起来比较复杂,自己因此也 花费了很多时间也学习相关基础知识,渐有心得,因此记录下来,供参考。


15.1. paste库

15.1.1. 程序示例

paste的核心是paste配置文件,因此看懂paste.ini配置文件是关键,先来看一个例子。

paste配置文件:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
[DEFAULT]
key1=value1
key2=value2
key3=values

[composite:pdl]
use=egg:Paste#urlmap
/:root
/calc:calc
#/v1:api_v1

#[app:api_v1]
#paste.app_factory = v1.router:MyRouterApp.factory

[pipeline:root]
pipeline = logrequest showversion

[pipeline:calc]
pipeline = test_filter calculator

[filter:logrequest]
username = root
password = root123
paste.filter_factory = pastedeploylab:LogFilter.factory

# add by chenshiqiang for test
[filter:test_filter]
k1 = m1
k2 = m2
paste.filter_factory = pastedeploylab:TestFilter.factory
# end add

[app:showversion]
version = 1.0.0
paste.app_factory = pastedeploylab:ShowVersion.factory

[app:calculator]
description = This is an "+-*/" Calculator
paste.app_factory = pastedeploylab:Calculator.factory

使用paste.ini生成服务端程序:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
#coding: utf-8

'''''
Created on 2011-6-12
@author: Sonic
'''

import os
import webob
from webob import Request
from webob import Response
from paste.deploy import loadapp
from wsgiref.simple_server import make_server
from time import sleep

#Filter

class TestFilter():
    def __init__(self,app):
        print "call test_filter init\n"
        self.app = app

    def __call__(self,environ,start_response):
        print "filter: call Test_Filter."
        return self.app(environ,start_response)

    @classmethod
    def factory(cls, global_conf, **kwargs):
        print "in Test_Filter.factory", global_conf, kwargs
        return TestFilter

class LogFilter():
    def __init__(self,app):
        print "call log filter init\n"
        self.app = app

    def __call__(self,environ,start_response):
        print "filter:LogFilter is called."
        return self.app(environ,start_response)

    @classmethod
    def factory(cls, global_conf, **kwargs):
        print "in LogFilter.factory", global_conf, kwargs
        return LogFilter

class ShowVersion():
    def __init__(self):
        print "call showversion init\n"

    def __call__(self,environ,start_response):
        print "showversion call"
        start_response("200 OK",[("Content-type", "text/plain")])
        return ["Paste Deploy LAB: Version = 1.0.0\n",]

    @classmethod
    def factory(cls,global_conf,**kwargs):
        print "in ShowVersion.factory", global_conf, kwargs
        return ShowVersion()



class Calculator():
    def __init__(self):
        print "call calc init\n"

    def __call__(self,environ, start_response):
        #print environ
        print "calculator call"
        req = Request(environ)
        res = Response()
        res.status = "200 OK"
        res.content_type = "text/plain"
        # get operands
        operator = req.GET.get("oper", None)
        operand1 = req.GET.get("op1", None)
        operand2 = req.GET.get("op2", None)
        print req.GET
        #print operand1, operand2
        #sleep(10)
        opnd1 = int(operand1)
        opnd2 = int(operand2)
        if operator == u'plus':
            opnd1 = opnd1 + opnd2
        elif operator == u'minus':
            opnd1 = opnd1 - opnd2
        elif operator == u'star':
            opnd1 = opnd1 * opnd2
        elif operator == u'slash':
            opnd1 = opnd1 / opnd2
        res.body = "%s \nRESULT = %d\n" % (str(req.GET) , opnd1)
        #return [res.body]
        return res(environ,start_response)
        #start_response("200 OK",[("Content-type", "text/plain")])
        #return ["call calc\n"]

    @classmethod
    def factory(cls,global_conf,**kwargs):
        print "in Calculator.factory", global_conf, kwargs
        return Calculator()

if __name__ == '__main__':
    configfile="pastedeploylab.ini"
    appname="pdl"
    #wsgi_app = loadapp("config:%s" % os.path.abspath(configfile), appname)
    wsgi_app = loadapp("config:%s" % os.path.abspath(configfile), "test_paste")
    server = make_server('localhost', 9999, wsgi_app)
    server.serve_forever()
    pass

运行服务端程序:

../_images/run_paste_test.png

运行程序:服务端

../_images/curl_paste.png

curl客户端测试

15.1.2. paste配置讲解

使用Paste和PasteDeploy模块来实现WSGI服务时,都需要一个paste.ini文件。 这个文件也是Paste框架的精髓,这里需要重点说明一下这个文件如何阅读。

paste.ini文件的格式类似于INI格式,每个section的格式为[type:name]。 这里重要的是理解几种不同type的section的作用。

composite

这种section用于将HTTP请求分发到指定的app。

app

这种section表示具体的app。

filter

实现一个过滤器中间件。关于wsgi中间件的知识可以参考 wsgi 基础

pipeline

用来把把一系列的filter串起来。

然后对照程序,来挨个分析每个section。

section composite

这种section用来决定如何分发HTTP请求。路由的对象其实就是paste.ini中其他secion的名字,类型必须是app或者pipeline。

[composite:pdl]
use=egg:Paste#urlmap        # use 为关键字
/:root                      # "/" 开头的请求路由给root(对应pipeline:root)处理
/calc:calc                  # "/calc" 开头的请求路由给calc处理
#/v1:api_v1

#[app:api_v1]
#paste.app_factory = v1.router:MyRouterApp.factory

section pipeline

pipeline是把filter和app串起来的一种section。它只有一个关键字就是pipeline。

[pipeline:root]
pipeline = logrequest showversion

[pipeline:calc]
pipeline = test_filter calculator

pipeline指定的section有如下要求:

  • 最后一个名字对应的section一定要是一个app;
  • 除最后一个名字外其他名字对应的section一定要是一个filter;

pipeline关键字指定了很多个名字,这些名字也是paste.ini文件中其他section的名字。 请求会从最前面的section开始处理,一直向后传递。

../_images/call_filter_first.png

filter定义在前,因此先调用filter,再调用app

section filter

filter是用来过滤请求和响应的,以WSGI中间件的方式实现。 其中的paste.filter_factory表示调用哪个函数来获得这个filter中间件。

[filter:logrequest]
username = root
password = root123
paste.filter_factory = pastedeploylab:LogFilter.factory
# 调用pastedeploylab 的LogFilter 类的factory函数获得 logrequest中间件!
# username 和 password是定义的变量。


# add by chenshiqiang for test
[filter:test_filter]
k1 = m1
k2 = m2
paste.filter_factory = pastedeploylab:TestFilter.factory
# 调用pastedeploylab 的TestFilter 的factory 函数获取 test_filter中间件!
# end add

section app

app表示实现主要功能的应用,是一个标准的WSGI application。 paste.app_factory表示调用哪个函数来获得这个app。

[app:showversion]
version = 1.0.0
paste.app_factory = pastedeploylab:ShowVersion.factory

[app:calculator]
description = This is an "+-*/" Calculator
paste.app_factory = pastedeploylab:Calculator.factory

15.1.3. loadapp() name参数

loadapp 函数的name很关键,必须和配置文件的某个 composite对应上,否则出错。 实际上,name变量表示paste.ini中一个section的名字,指定这个section作为HTTP请求处理的第一站。

#wsgi_app = loadapp("config:%s" % os.path.abspath(configfile), appname)
wsgi_app = loadapp("config:%s" % os.path.abspath(configfile), "test_paste")
../_images/modify_app_name_error.png

更改name 参数程序报错

15.1.4. URL匹配

路径不是绝对匹配,只要开头匹配就可以,但是假如有更精确的匹配,那就采用更精确的。(有点类似于IP路由的最长最佳路由匹配)

../_images/paste_head_match.png

开头匹配和最长精确匹配

15.1.5. 总结

paste.ini中这一大堆配置的作用就是把我们用Python写的WSGI application和middleware串起来,规定好HTTP请求处理的路径。 即规定,对哪个URL path 调用对应的app!

Note

如果使用wsgiref.make_server创建一个server,只有一个app,那么所以的请求都会使用该app处理! 可以参考 wsgi 基础

[1]https://www.ustack.com/blog/demoapi2/
[2]http://blog.csdn.net/sonicatnoc/article/details/6539716

15.2. webob库

WebOb is a Python library that provides wrappers around the WSGI request environment, and an object to help create WSGI responses. The objects map much of the specified behavior of HTTP, including header parsing, content negotiation and correct handling of conditional and range requests.

来看例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
#coding:utf-8

import webob
import webob.dec
import webob.exc

'''
我们知道,wsgi规定了web server和web app之间的接口。
但是通过environ 传递http request信息,有点不太直观,并且
使用start_response 回调函数,逻辑也显得比较难以理解。

一般而言,web app的正常调用逻辑是:

res = func(req)

即func接收HTTP req请求,并返回HTTP response。但是func的接口,
并不符合wsgi的规定。因此webob就派上用场了,该库的任务
之一就是把普通的函数(后面的例子可以看到,函数参数和返回类型
有限制)通过装饰器包装,转化成标准的wsgi app。
'''

#==================================================#

"""
webob 测试程序一:
从这里可以看到,我们只要定义函数,接收 `webob.Request`类型的参数,
并返回`webob.Response`类型的对象。我们就可以通过`wsgify`装饰器进行
包装,使之成为标准的wsgi app(接收environ, start_response参数的app)
"""

@webob.dec.wsgify
def myfunc(req):
    print req
    print type(req)
    return webob.Response('hey there\n')
    #return 'hey there\n'
    #return u"1900"

#myfunc = webob.dec.wsgify(myfunc)

#==================================================#

"""
webob 测试程序二:

程序一中req是<class 'webob.request.Request'> 类型的(可以通过
print type查看)。此外,req参数也可以是Request class的子类。

这里,我们自定义MyRequest 继承自Request,然后传给装饰器参数,
因此myfun2 的req类型是MyRequest.
"""

class MyRequest(webob.Request):
    @property
    def is_local(self):
        return self.remote_addr == '127.0.0.1'

@webob.dec.wsgify(RequestClass=MyRequest)
def myfunc2(req):
    print type(req)
    if req.is_local:
        return webob.Response('hi!\n')
    else:
        raise webob.exc.HTTPForbidden

#==================================================#

from wsgiref.simple_server import make_server

#httpd = make_server('127.0.0.1', 9999, myfunc)
#httpd = make_server('127.0.0.1', 9999, myfunc2)
httpd = make_server('0.0.0.0', 9999, myfunc2)
#httpd = make_server('0.0.0.0', 9999, myfunc)
httpd.serve_forever()

可以看到,myfunc,myfunc2的接口不是wsgi规范定义的。但是,通过wsgify包装后,他们 已经是wsgi app,并可启动、响应http请求!

root@juno-controller:/smbshare/paste_test# curl   http://localhost:9999/dumm
hi!
root@juno-controller:/smbshare/paste_test# curl   http://192.168.60.254:9999/dumm
<html>
 <head>
  <title>403 Forbidden</title>
 </head>
 <body>
  <h1>403 Forbidden</h1>
  Access was denied to this resource.<br /><br />
 </body>

先了解webob的简单用法,等分析openstack源码遇到更高级用法时再近些分析!

15.3. routes库

restful程序的一大特点是url和app对应起来,但是wsgi规范和webob并没有 解决这个问题。这就是routes库解决的问题,来看官网说明:

Routes is a Python re-implementation of the Rails routes system for mapping URL’s to Controllers/Actions and generating URL’s. Routes makes it easy to create pretty and concise URL’s that are RESTful with little effort.

来一个例子:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
#coding:utf-8

from __future__ import print_function
from routes import Mapper
import webob.dec
import webob.exc
import routes.middleware
import testtools

'''
该程序如如下目的:

1. 测试OpenStack rest服务的处理逻辑,顺序为:

   Python Delopy -- > MyRouter --> routes.middleware.RoutesMiddleware 
   --> MyApplication --> MyController

2. 测试wsgi app返回Unicode字符串问题。curl请求会报错!
   所以需要判断,假如是Unicode对象,需要手动转换成str

'''

class MyController(object):
    def getlist(self, mykey):
        print("step 4: MyController's getlist(self, mykey) is invoked")
        #return "getlist(), mykey=" + mykey
        print ('mykey:%s'%type(mykey))
        ret = "getlist(), mykey=%s\n"%mykey
        print ('ret:%s'%type(ret))
        return ret

class MyApplication(object):
    """Test application to call from router."""

    def __init__(self, controller):
        self._controller = controller

    def __call__(self, environ, start_response):
        print("step 3: MyApplication is invoked")

        action_args = environ['wsgiorg.routing_args'][1].copy()
        try:
            del action_args['controller']
        except KeyError:
            pass

        try:
            del action_args['format']
        except KeyError:
            pass

        action = action_args.pop('action', None)
        controller_method = getattr(self._controller, action)
        result = controller_method(**action_args)

        start_response('200 OK', [('Content-Type', 'text/plain')])
        # wsgi app 不能返回Unicode对象!所以需要转换成字节串.
        if type(result) == type(u'o'):
            result = str(result)
        return [result]

        # 不能用改行,否则会提示Response 对象不可以迭代错误!
        #return webob.Response(result)

class MyRouter(object):
    """Test router."""

    def __init__(self):
        route_name = "dummy_route"
        route_path = "/dum"

        my_application = MyApplication(MyController())
        #my_application = MyApp2(MyController())

        self.mapper = Mapper()
        self.mapper.connect(route_name, route_path,
                        controller=my_application,
                        #action="getlist-2",
                        action="getlist",
                        mykey="myvalue",
                        conditions={"method": ['GET']})

        self._router = routes.middleware.RoutesMiddleware(self._dispatch,
                                                          self.mapper)

    @webob.dec.wsgify(RequestClass=webob.Request)
    def __call__(self, req):
        """Route the incoming request to a controller based on self.map.

        If no match, return a 404.

        """
        print("step 1: MyRouter is invoked")
        return self._router

    @staticmethod
    @webob.dec.wsgify(RequestClass=webob.Request)
    def _dispatch(req):
        """Dispatch the request to the appropriate controller.

        Called by self._router after matching the incoming request to a route
        and putting the information into req.environ.  Either returns 404
        or the routed WSGI app's response.

        """
        print("step 2: RoutesMiddleware is invoked, calling our _dispatch back")

        match_dict = req.environ['wsgiorg.routing_args'][1]
        if not match_dict:
            return webob.exc.HTTPNotFound()
        app = match_dict['controller']
        return app

from wsgiref.simple_server import make_server

httpd = make_server('0.0.0.0', 9999, MyRouter())
httpd.serve_forever()

Important

Each route in mapper must specify a ‘controller’, which is a WSGI app to call. You’ll probably want to specify an ‘action’ as well and have your controller be an object that can route the request to the action-specific method.

通过这段文字可以知道,我们mapper.connect()的controller参数就是和url关联的 WSGI app。如果指定了action参数,那么请求会路由到该特定的方法!