通過demo學習OpenStack開發--API服務(2)
編者按:《通過demo學習OpenStack開發》專欄是劉陳泓的系列文章,專欄通過開發一個demo的形式來介紹一些參與OpenStack項目開發的必要的基礎知識,希望幫助大家入門企業級Python項目的開發和OpenStack項目的開發。劉陳泓主要關注OpenStack的身份認證和計費領域。另外,還對云計算、分布式系統應用和開發感興趣。
本文會重點講解OpenStack中使用的API開發框架的使用。但是本文的目的并不是覆蓋這些框架的使用細節,而是通過說明重要的部分,降低初學者的入門的門檻。框架的使用細節都可以從文檔中找到。說明一下,除非特殊說明,本文中的相對路徑都是相對于項目源碼目錄的相對路徑。
Paste + PasteDeploy + Routes + WebOb
我們在API服務(1)中已經提到了,這個框架只在早期開始的項目中使用,新的項目都已經轉到Pecan框架了。但是,早期的項目都是比較核心的項目,因此我們還是要學會如何使用這個框架。我們會以Keystone項目為例,來說明如何閱讀使用這個框架的開發的API代碼。
重點在于確定URL路由
RESTful API程序的主要特點就是URL path會和功能對應起來。這點從API文檔就可以看得出來,比如用戶管理的功能一般都放在/user這個路徑下。因此,看一個RESTful API程序,一般都是看它實現了哪些URL path,以及每個path對應了什么功能,這個一般都是由框架的URL路由功能負責的。所以,熟悉一個RESTful API程序的重點在于確定URL路由。本章所說的這個框架對于初學者的難點也是如何確定URL路由。
WSGI入口和中間件
作為基礎知識,你需要先了解一下WSGI的相關概念,可以參考這篇文章 WSGI簡介 。
WSGI入口
在API服務(1)中提到了WSGI可以使用Apache進行部署,也可以使用eventlet進行部署。Keystone項目同時提供了這兩種方案的代碼,也就是我們要找的WSGI的入口。
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 對象。
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() 函數,這個函數的內容也很簡單:
def main(): eventlet_server.run(possible_topdir)
main() 函數的主要作用就是啟動一個eventlet_server,配置文件從 possible_topdir 中查找。因為eventlet的部署方式涉及到eventlet庫的使用方法,本文不再展開說明。讀者可以在學會確定URL路由后再回來看這個代碼。下面,繼續以httpd/keystone.py文件作為入口來說明如何閱讀代碼。
Paste和PasteDeploy
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很關鍵
在上面的代碼中我們可以看到, name 這個變量從 httpd/keystone.py 文件傳遞到 initialize_application() 函數,又被傳遞到 keystone_service.loadapp() 函數,最終被傳遞到 paste.deploy.loadapp() 函數。那么,這個 name 變量到底起什么作用呢?先把這個問題放在一邊,我們后面再來解決它。
paste.ini
使用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中去了。
最后的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項目的代碼結構
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項目的結構,其他項目即使用了同樣的框架,也不一定是這么做的。
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中的各種庫,這些庫的相關文檔都可以在項目 官網 找到:。
另外,routes庫的 項目官網 。
來自: http://www.infoq.com/cn/articles/OpenStack-UnitedStack-API2