OpenStack对外提供REST API,那么REST API是如何建立起来的呢?
OpenStack用了一种叫WSGI(Web Service Gateway Interface)的东西,WSGI是Web服务器与Web应用程序或应用框架之间的一种低级别的接口。
下面是个简单的例子:
#!/usr/bin/env python
from wsgiref.simple_server import make_server
def hello_world_app(environ, start_response): status = '200 OK' # HTTP Status headers = [('Content-type', 'text/plain')] # HTTP Headers start_response(status, headers) # The returned object is going to be printed return ["Hello World"] httpd = make_server('', 8088, hello_world_app) print "Serving on port 8088..." # Serve until process is killed httpd.serve_forever()
执行这段代码,然后浏览器访问http://localhost:8088就会出现Hello World页面。
大致流程如下:
(1)Client(上例中浏览器)发送请求到Server。
(2)Server转发请求给Application(上例中hello_world_app)。注:Server和Application之间还有middleware,此处省略。
(3)Application进行操作后将相应发送给Server。
(4)Server再将相应转发给Client。
OpenStack使用WSGI的一个工具包paste来配置WSGI appliaction和server的系统,它的好处是将配置和代码分离。python代码写好后如果想要修改页面到app的映射关系,只需要修改配置文件即可。
用一个简单的例子来示范paste.deploy的工作机制:
pastedeploy.ini
[composite:test_composite]
use=egg:Paste#urlmap
/:root
[pipeline:root]
pipeline = logrequest showversion
[filter:logrequest]
username = root
password = root123
paste.filter_factory = pastedeploylab:LogFilter.factory
[app:showversion]
version = 1.0.0
paste.app_factory = pastedeploylab:ShowVersion.factory
app:表示它定义了一个wsgi的application,是一个callable对象。paste. app_factory返回值是一个application对象
filter:表示这个段定义了一个filter,filter需要完成的工作是将application包装成另一个application(“过滤”),并返回这个包装后的application。
pipeline:Pipeline 由一些列的filter组成,最后一个是应用,即将前面的fiiter应用到application。
composite:自己不处理请求,根据映射关系把请求分发到filter、app或者pipeline。/:root就是表示访问url根目录的请求全部分发到root这个pipeline处理
pastedeploy.py
import os
import webob
from webob import Request
from webob import Response
from paste.deploy import loadapp
from wsgiref.simple_server import make_server
#Filter
class LogFilter():
def __init__(self,app):
self.app = app
pass
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):
pass
def __call__(self,environ,start_response):
start_response("200 OK",[("Content-type", "text/plain")])
return ["Paste Deploy LAB: Version = 1.0.0",]
@classmethod
def factory(cls,global_conf,**kwargs):
print "in ShowVersion.factory", global_conf, kwargs
return ShowVersion()
if __name__ == '__main__':
configfile="pastedeploy.ini"
appname="test_composite"
wsgi_app = loadapp("config:%s" % os.path.abspath(configfile), appname)
server = make_server('localhost',8080,wsgi_app)
server.serve_forever()
pass
执行命令python pastedeploy.py,然后在浏览器中输入http://localhost:8080/就可以在网页输出Paste Deploy LAB: Version = 1.0.0
下面讲解一下工作流程,运行pastedeploy.py文件,首先会调用loadapp函数加载运用,在配置文件pastedeploy.ini找到appname为test_composite,test_composite是一个composite,然后找到pipeline root,根据pipeline找到filter logrequest和app showversion,logrequest和showversion各自用factory生成callable对象。加载完应用后调用make_server启动服务。
在浏览器输入http://localhost:8080/就会根据urlmap将请求分发到pipeline root,调用LogFilter的__call__方法,其中app就是ShowVersion,然后调用ShowVersion的__call__方法返回消息。
以下写一个简单的OpenStack WSGI实例,参考了臭蛋的博客,臭蛋写的和OpenStack源码很一致。
其中用到的一些python库:
1. paste.deploy 配置WSGI appliaction和server
2. webob 用来对http请求和响应进行封装
3. routes 实现URL映射
4. eventlet.wsgi 或者 wsgiref.simple_server,提供wsgi server功能,后者更简单。
首先建立一个test包,然后在test包里面建立如下文件:
test-paste.ini
[composite:test_composite]
use=egg:Paste#urlmap
/v1:testapp
[app:testapp]
paste.app_factory = test.router:API.factory
server.py
import os
import logging
import sys
from paste import deploy
from wsgiref.simple_server import make_server
LOG = logging.getLogger(__name__)
module_dir = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]),
os.pardir,os.pardir))
sys.path.insert(0,module_dir)
bind_host = "127.0.0.1"
bind_port = 8088
def server(app_name, conf_file):
print "server"
app = load_paste_app(app_name,conf_file)
serve = make_server(bind_host,bind_port,app)
serve.serve_forever()
def load_paste_app(app_name, conf_file):
print "load_paste_app"
LOG.debug("Loading %(app_name) from %(conf_file)",
{'app_name':app_name, 'conf_file':conf_file})
try:
app = deploy.loadapp("config:%s" % os.path.abspath(conf_file), name=app_name)
return app
except (LookupError, ImportError) as e:
LOG.error(str(e))
raise RuntimeError(str(e))
if __name__ == '__main__':
app_name = "test_composite"
conf_file = "test-paste.ini"
server(app_name,conf_file)
wsgi.py
import logging
import routes.middleware
import webob.dec
import webob.exc
class Router(object):
def __init__(self, mapper=None):
print "Router.__init__"
self.map = mapper
self._router = routes.middleware.RoutesMiddleware(self._dispatch,
self.map)
@classmethod
def factory(cls, global_conf, **local_conf):
print "Router.__factory__"
return cls()
@webob.dec.wsgify
def __call__(self,req):
print "Router.__call__"
return self._router
@staticmethod
@webob.dec.wsgify
def _dispatch(req):
print "Router._dispatch"
# TODO
match = req.environ['wsgiorg.routing_args'][1]
if not match:
return webob.exc.HTTPNotFound()
app = match['controller']
return app
router.py
import routes
from test import wsgi
from test import versions
class API(wsgi.Router):
def __init__(self, mapper=None):
print "API.__init__"
if(mapper == None):
mapper = routes.Mapper()
versions_resource = versions.create_resource()
mapper.connect("/test",controller=versions_resource,
action="index")
super(API,self).__init__(mapper)
versions.py
import httplib
import json
import webob.dec
from test import wsgi
from webob import Response
class Controller(object):
def __init__(self):
print "Controller.__init__"
# TODO
self.version = "0.1"
def index(self,req):
print "Controller.index"
response = Response(request=req,
status=httplib.MULTIPLE_CHOICES,
content_type='application/json')
response.body = json.dumps(dict(versions=self.version))
return response
@webob.dec.wsgify
def __call__(self, request):
print "Controller.__call__"
# TODO
return self.index(request)
def create_resource():
print "create_resource"
return Controller()
@webob.dec.wsgify 装饰器将一个普通函数转变成WSGI应用程序
执行python server.py , 然后在浏览器输入http://localhost:8088/v1/test 就会出现相关页面。
由于在函数中加了打印语句,启动时会输出:
server
load_paste_app
Router.__factory__
API.__init__
create_resource
Controller.__init__
Router.__init__
访问页面会输出:
Router.__call__
Router._dispatch
Controller.__call__
Controller.index
这是一个OpenStack WSGI原型,还需完善,比如在router.py文件中,/test并没有和index方法绑定,只是在Controller.__call__方法中静态的调用了index方法。