本文會重點講解OpenStack中使用的API開發框架的使用。可是本文的目的並非覆蓋這些框架的使用細節,而是經過說明重要的部分,下降初學者的入門的門檻。框架的使用細節均可以從文檔中找到。說明一下,除非特殊說明,本文中的相對路徑都是相對於項目源碼目錄的相對路徑。python
咱們在API服務(1)中已經提到了,這個框架只在早期開始的項目中使用,新的項目都已經轉到Pecan框架了。可是,早期的項目都是比較核心的項目,所以咱們仍是要學會如何使用這個框架。咱們會以Keystone項目爲例,來講明如何閱讀使用這個框架的開發的API代碼。web
RESTful API程序的主要特色就是URL path會和功能對應起來。這點從API文檔就能夠看得出來,好比用戶管理的功能通常都放在/user這個路徑下。所以,看一個RESTful API程序,通常都是看它實現了哪些URL path,以及每一個path對應了什麼功能,這個通常都是由框架的URL路由功能負責的。因此,熟悉一個RESTful API程序的重點在於肯定URL路由。本章所說的這個框架對於初學者的難點也是如何肯定URL路由。json
做爲基礎知識,你須要先了解一下WSGI的相關概念,能夠參考這篇文章WSGI簡介。segmentfault
在API服務(1)中提到了WSGI可使用Apache進行部署,也可使用eventlet進行部署。Keystone項目同時提供了這兩種方案的代碼,也就是咱們要找的WSGI的入口。api
Keystone項目在httpd/目錄下,存放了能夠用於Apache服務器部署WSGI服務的文件。其中,wsgi-keystone.conf是一個mod_wsgi的示例配置文件,keystone.py則是WSGI應用程序的入口文件。httpd/keystone.py也就是咱們要找的入口文件之一。這個文件的內容很簡單:服務器
import os from keystone.server import wsgi as wsgi_server name = os.path.basename(__file__) application = wsgi_server.initialize_application(name)
文件中建立了WSGI入口須要使用的application
對象。app
keystone-all命令則是採用eventlet來進行部署時的入口,能夠從setup.cfg文件按中肯定keystone-all命令的入口:框架
[entry_points] console_scripts = keystone-all = keystone.cmd.all:main keystone-manage = keystone.cmd.manage:main
從setup.cfg文件的entry_points部分能夠看出,keystone-all的入口是keystone/cmd/all.py文件中的main()
函數,這個函數的內容也很簡單:python2.7
def main(): eventlet_server.run(possible_topdir)
main()
函數的主要做用就是啓動一個eventlet_server,配置文件從possible_topdir
中查找。由於eventlet的部署方式涉及到eventlet庫的使用方法,本文再也不展開說明。讀者能夠在學會肯定URL路由後再回來看這個代碼。下面,繼續以httpd/keystone.py文件做爲入口來講明如何閱讀代碼。ide
httpd/keystone.py中調用的initialize_application(name)
函數載入了整個WSGI應用,這裏主要用到了Paste和PasteDeploy庫。
def initialize_application(name): ... def loadapp(): return keystone_service.loadapp( 'config:%s' % config.find_paste_config(), name) _unused, application = common.setup_backends( startup_application_fn=loadapp) return application
上面是刪掉無關代碼後的initialize_application()
函數。config.find_paste_config()
用來查找PasteDeploy須要用到的WSGI配置文件,這個文件在源碼中是etc/keystone-paste.ini文件,若是在線上環境中,通常是/etc/keystone-paste.init。keystone_service.loadapp()
函數內部則調用了paste.deploy.loadapp()
函數來加載WSGI應用,如何加載則使用了剛纔提到的keystone-paste.ini文件,這個文件也是看懂整個程序的關鍵。
在上面的代碼中咱們能夠看到,name
這個變量從httpd/keystone.py文件傳遞到initialize_application()
函數,又被傳遞到keystone_service.loadapp()
函數,最終被傳遞到paste.deploy.loadapp()
函數。那麼,這個name
變量到底起什麼做用呢?先把這個問題放在一邊,咱們後面再來解決它。
使用Paste和PasteDeploy模塊來實現WSGI服務時,都須要一個paste.ini文件。這個文件也是Paste框架的精髓,這裏須要重點說明一下這個文件如何閱讀。
paste.ini文件的格式相似於INI格式,每一個section的格式爲[type:name]。這裏重要的是理解幾種不一樣type的section的做用。
composite: 這種section用於將HTTP請求分發到指定的app。
app: 這種section表示具體的app。
filter: 實現一個過濾器中間件。
pipeline: 用來把把一系列的filter串起來。
上面這些section是在keystone的paste.ini中用到的,下面詳細介紹一下如何使用。這裏須要用到WSGIMiddleware(WSGI中間件)的知識,能夠在WSGI簡介這篇文章中找到。
section composite
這種section用來決定如何分發HTTP請求。Keystone的paste.ini文件中有兩個composite的section:
[composite:main] use = egg:Paste#urlmap /v2.0 = public_api /v3 = api_v3 / = public_version_api [composite:admin] use = egg:Paste#urlmap /v2.0 = admin_api /v3 = api_v3 / = admin_version_api
在composite seciont中,use是一個關鍵字,指定處理請求的代碼。egg:Paste#urlmap
表示到Paste模塊的egg-info中去查找urlmap關鍵字所對應的函數。在virtualenv環境下,是文件/lib/python2.7/site-packages/Paste-2.0.2.dist-info/metadata.json:
{ ... "extensions": { ... "python.exports": { "paste.composite_factory": { "cascade": "paste.cascade:make_cascade", "urlmap": "paste.urlmap:urlmap_factory" }, ... }
在這個文件中,你能夠找到urlmap
對應的是paste.urlmap:urlmap_factory
,也就是paste/urlmap.py文件中的urlmap_factory()
函數。
composite section中其餘的關鍵字則是urlmap_factory()
函數的參數,用於表示不一樣的URL path前綴。urlmap_factory()
函數會返回一個WSGI app,其功能是根據不一樣的URL path前綴,把請求路由給不一樣的app。以[composite:main]爲例:
[composite:main] use = egg:Paste#urlmap /v2.0 = public_api # /v2.0 開頭的請求會路由給public_api處理 /v3 = api_v3 # /v3 開頭的請求會路由個api_v3處理 / = public_version_api # / 開頭的請求會路由給public_version_api處理
路由的對象其實就是paste.ini中其餘secion的名字,類型必須是app或者pipeline。
section pipeline
pipeline是把filter和app串起來的一種section。它只有一個關鍵字就是pipeline。咱們以api_v3這個pipeline爲例:
[pipeline:api_v3] # The last item in this pipeline must be service_v3 or an equivalent # application. It cannot be a filter. pipeline = sizelimit url_normalize request_id build_auth_context token_auth admin_token_auth json_body ec2_extension_v3 s3_extension simple_cert_extension revoke_extension federation_extension oauth1_extension endpoint_filter_extension endpoint_policy_extension service_v3
pipeline關鍵字指定了不少個名字,這些名字也是paste.ini文件中其餘section的名字。請求會從最前面的section開始處理,一直向後傳遞。pipeline指定的section有以下要求:
最後一個名字對應的section必定要是一個app
非最後一個名字對應的section必定要是一個filter
section filter
filter是用來過濾請求和響應的,以WSGI中間件的方式實現。
[filter:sizelimit] paste.filter_factory = oslo_middleware.sizelimit:RequestBodySizeLimiter.factory
這個是api_v3這個pipeline指定的第一個filter,做用是限制請求的大小。其中的paste.filter_factory表示調用哪一個函數來得到這個filter中間件。
section app
app表示實現主要功能的應用,是一個標準的WSGI application。
[app:service_v3] paste.app_factory = keystone.service:v3_app_factory
paste.app_factory表示調用哪一個函數來得到這個app。
總結一下
paste.ini中這一大堆配置的做用就是把咱們用Python寫的WSGI application和middleware串起來,規定好HTTP請求處理的路徑。
name是用來肯定入口的
上面咱們提到了一個問題,就是name
變量的做用究竟是什麼?name
變量表示paste.ini中一個section的名字,指定這個section做爲HTTP請求處理的第一站。在Keystone的paste.ini中,請求必須先由[composite:main]或者[composite:admin]處理,因此在keystone項目中,name
的值必須是main或者admin。
上面提到的httpd/keystone.py文件中,name等於文件名的basename,因此實際部署中,必須把keystone.py重命名爲main.py或者admin.py。
舉個例子
通常狀況下,從Keystone服務獲取一個token時,會使用下面這個API:
POST http://hostname:35357/v3/auth/tokens
咱們根據Keystone的paste.ini來講明這個API是如何被處理的:
hostname:35357 這一部分是由Web服務器處理的,好比Apache。而後,請求會被轉到WSGI的入口,也就是httpd/keystone.py中的application
對象取處理。
application
對象是根據paste.ini中的配置來處理的。這裏會先由[composite:admin]來處理(通常是admin監聽35357端口,main監聽5000端口)。
[composite:admin]發現請求的path是/v3開頭的,因而就把請求轉發給[pipeline:api_v3]去處理,轉發以前,會把/v3這個部分去掉。
[pipeline:api_v3]收到請求,path是/auth/tokens,而後開始調用各個filter來處理請求。最後會把請求交給[app:service_v3]進行處理。
[app:service_v3]收到請求,path是/auth/tokens,而後交給最終的WSGI app去處理。
下一步
到此爲止,paste.ini中的配置的全部工做都已經作完了。下面請求就要轉移到最終的app內部去處理了。前面已經說過了,咱們的重點是肯定URL路由,那麼如今還有一部分的path的路由還沒肯定,/auth/tokens,這個還須要下一步的工做。
上面咱們提到paste.ini中用到了許多的WSGI中間件,那麼這些中間件是如何實現的呢?咱們來看一個例子就知道了。
[filter:build_auth_context] paste.filter_factory = keystone.middleware:AuthContextMiddleware.factory
build_auth_context這個中間件的做用是在WSGI的environ中添加KEYSTONE_AUTH_CONTEXT這個鍵,包含的內容是認證信息的上下文。實現這個中間件的類繼承關係以下:
keystone.middleware.core.AuthContextMiddleware -> keystone.common.wsgi.Middleware -> keystone.common.wsgi.Application -> keystone.common.wsgi.BaseApplication
這裏實現的關鍵主要在前面兩個類中。
keystone.common.wsgi.Middleware類實現了__call__()
方法,這個就是WSGI中application端被調用時運行的方法。
class Middleware(Application): ... @webob.dec.wsgify() def __call__(self, request): try: response = self.process_request(request) if response: return response response = request.get_response(self.application) return self.process_response(request, response) except exceptin.Error as e: ... ...
__call__()方法實現爲接收一個request對象,返回一個response對象的形式,而後使用WebOB模塊的裝飾器webob.dec.wsgify()
將它變成標準的WSGI application接口。這裏的request和response對象分別是 webob.Request
和webob.Response
。這裏,__call__()
方法內部調用了self.process_request()
方法,這個方法在keystone.middleware.core.AuthContextMiddleware中實現:
class AuthContextMiddleware(wsgi.Middleware): ... def process_request(self, request): ... request.environ[authorization.AUTH_CONTEXT_ENV] = auth_context
這個函數會根據功能設計建立auth_context
,而後賦值給request.environ['KEYSTONE_AUTH_CONTEXT
]`,這樣就能經過WSGI application方法的environ傳遞到下一個WSGI application中去了。
上面咱們已經看到了,對於/v3開頭的請求,在paste.ini中會被路由到[app:service_v3]這個section,會交給keystone.service:v3_app_factory
這個函數生成的application處理。最後這個application須要根據URL path中剩下的部分,/auth/tokens,來實現URL路由。從這裏開始,就須要用到Routes模塊了。
一樣因爲篇幅限制,咱們只能展現Routes模塊的大概用法。Routes模塊是用Python實現的相似Rails的URL路由系統,它的主要功能就是把path映射到對應的動做。
Routes模塊的通常用法是建立一個Mapper
對象,而後調用該對象的connect()
方法把path和method映射到一個controller的某個action上,這裏controller是一個自定義的類實例,action是表示controller對象的方法的字符串。通常調用的時候還會指定映射哪些方法,好比GET或者POST之類的。
舉個例子,來看下keystone/auth/routers.py的代碼:
class Routers(wsgi.RoutersBase): def append_v3_routers(self, mapper, routers): auth_controller = controllers.Auth() self._add_resource( mapper, auth_controller, path='/auth/tokens', get_action='validate_token', head_action='check_token', post_action='authenticate_for_token', delete_action='revoke_token', rel=json_home.build_v3_resource_relation('auth_tokens')) ...
這裏調用了本身Keystone本身封裝的_add_resource()方法批量爲一個/auth/tokens這個path添加多個方法的處理函數。其中,controller是一個controllers.Auth實例,也就是 keystone.auth.controllers.Auth。其餘的參數,咱們從名稱能夠猜出其做用是指定對應方法的處理函數,好比get_action用於指定GET方法的處理函數爲validate_token。咱們再深刻一下,看下_add_resource()這個方法的實現:
def _add_resource(self, mapper, controller, path, rel, get_action=None, head_action=None, get_head_action=None, put_action=None, post_action=None, patch_action=None, delete_action=None, get_post_action=None, path_vars=None, status=json_home.Status.STABLE): ... if get_action: getattr(controller, get_action) # ensure the attribute exists mapper.connect(path, controller=controller, action=get_action, conditions=dict(method=['GET'])) ...
這個函數其實很簡單,就是調用mapper對象的connect方法指定一個path的某些method的處理函數。
Keystone項目把每一個功能都分到單獨的目錄下,好比token相關的功能是放在keystone/token/目錄下,assignment相關的功能是放在keystone/assignment/目錄下。目錄下都通常會有三個文件:routers.py, controllers.py, core.py。routers.py中實現了URL路由,把URL和controllers.py中的action對應起來;controllers.py中的action調用core.py中的底層接口實現RESTful API承諾的功能。因此,咱們要進一步肯定URL路由是如何作的,就要看routers.py文件。
注意,這個只是Keystone項目的結構,其餘項目即便用了一樣的框架,也不必定是這麼作的。
每一個模塊都定義了本身的路由,可是這些路由最終要仍是要經過一個WSGI application來調用的。上面已經提到了,在Keystone中,/v3開頭的請求最終都會交給keystone.service.v3_app_factory
這個函數生成的application來處理。這個函數裏也包含了路由最後分發的祕密,咱們來看代碼:
def v3_app_factory(global_conf, **local_conf): ... mapper = routes.Mapper() ... router_modules = [auth, assignment, catalog, credential, identity, policy, resource] ... for module in router_modules: routers_instance = module.routers.Routers() _routers.append(routers_instance) routers_instance.append_v3_routers(mapper, sub_routers) # Add in the v3 version api sub_routers.append(routers.VersionV3('public', _routers)) return wsgi.ComposingRouter(mapper, sub_routers)
v3_app_factory()
函數中先遍歷了全部的模塊,將每一個模塊的路由都添加到同一個mapper對象中,而後把mapper對象做爲參數用於初始化wsgi.ComposingRouter對象,因此咱們能夠判斷,這個wsgi.ComposingRouter對象必定是一個WSGI application,咱們看看代碼就知道了:
class Router(object): """WSGI middleware that maps incoming requests to WSGI apps.""" def __init__(self, mapper): self.map = mapper self._router = routes.middleware.RoutesMiddleware(self._dispatch, self.map) @webob.dec.wsgify() def __call__(self, req): return self._router ... class ComposingRouter(Router): def __init__(self, mapper=None, routers=None): ...
上述代碼證明了咱們的猜想。這個ComposingRouter對象被調用時(在其父類Router中實現),會返回一個WSGI application。這個application中則使用了routes模塊的中間件來實現了請求路由,在routes.middleware.RoutesMiddleware中實現。這裏對path進行路由的結果就是返回各個模塊的controllers.py中定義的controller。各個模塊的controller都是一個WSGI application,這個你能夠經過這些controller的類繼承關係看出來。
可是這裏只講到了,routes模塊把path映射到了一個controller,可是如何把對path的處理映射到controller的方法呢?這個能夠從controller的父類keystone.common.wsgi.Application的實現看出來。這個Application類中使用了environ['wsgiorg.routing_args']
中的數據來肯定調用controller的哪一個方法,這些數據是由上面提到的routes.middleware.RoutesMiddleware設置的。
到這裏咱們大概把Paste + PasteDeploy + Routes + WebOb這個框架的流程講了一遍,從本文的長度你就能夠看出這個框架有多囉嗦,用起來有多麻煩。下一篇文章咱們會講Pecan框架,咱們的demo也將會使用Pecan框架來開發。
本文主要提到了Python Paste中的各類庫,這些庫的相關文檔均可以在項目官網找到:http://pythonpaste.org/。
另外,routes庫的項目官網是:https://routes.readthedocs.org/en/latest/