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
|
运行服务端程序:
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。
-
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开始处理,一直向后传递。
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")
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参数,那么请求会路由到该特定的方法!