閱讀373 返回首頁    go 阿裏雲 go 技術社區[雲棲]


Openstack Restful API 開發框架 Paste + PasteDeploy + Routes + WebOb

Paste + PasteDeploy + Routes + WebOb 簡介

  • 由 Paste + PasteDeploy 完成 Application 的 WSGI 化,其中 PasteDeploy 完成 WSGI Server 和 Application 的構建;
  • Routes 負責 URL 路由轉發;
  • WebOb 完成了 WSGI 請求和響應的封裝 。

使用該框架開發出來的 Restful API 能夠滿足 WSGI 規範的要求,但是弊端在於該框架比較複雜,代碼量大。隻有最初的幾個 Openstack 核心項目在使用,後來的新生項目使用了一個相對而言更加簡單便捷的 Pecan 框架。

RESTful API 程序的主要特點就是 URL_Path 會和功能對應起來。比如用戶管理的功能一般都放在 https://hostname:post/version/<project_id>/user 這個路徑下。因此,看一個 RESTful API 程序,一般都是看它實現了哪些 UR_Path,以及每個 URL_Path 對應了什麼功能,這個一般都是由 Routes 的 URL 路由功能負責的。所以,熟悉一個RESTful API程序的重點在於確定URL路由。

WSGI入口

WSGI 可以使用 Apache 充當 Web Server,也可以使用 eventlet 進行部署,Keystone Project 都提供了這兩種方案的代碼實現(也就是 WSGI 的入口)。Keystone Project 在 keystone/httpd/ 目錄下,存放了 Apache Web Server 用於部署 WSGI 的相關文件:

  • wsgi-keystone.conf 是 mod_wsgi 功能模塊的示例配置文件,其中定義了 Post :5000 和 Post :35357 兩個虛擬主機,可以通過 Request URL_Path 中 Post 的不同來指定使用哪一個虛擬主機的 WSGI Application 來處理這一個請求。

  • keystone.py 則是 WSGI Application 的入口文件,Web Server 會根據配置路徑來加載這個入口文件,在該文件中包含了由 wsgi Module(wsgi.py) 提供的 application 可調用對象,該對象就是 WSGI Application 真正的入口。

Apache 部署方式的 WSGI 入口 :

#keystone/httpd/keystone.py

import os

from keystone.server import wsgi as wsgi_server

name = os.path.basename(__file__)

# NOTE(ldbragst): 'application' is required in this context by WSGI spec.
# Web Server 隻能通過 application 可調用對象來與 Application 進行交互
application = wsgi_server.initialize_application(name)

keystone/httpd/keystone.py 調用了 keystone/keystone/server/wsgi.py 模塊中的 initialize_application(name) 函數來載入 WSGI Application,這裏主要用到了 Paste + PasteDeploy 庫。

#keystone/keystone/server/wsgi.py

def initialize_application(name):

    def loadapp():
        return keystone_service.loadapp(
            'config:%s' % config.find_paste_config(), name)
        #keystone_service.loadapp() 函數內部調用了 paste.deploy.loadapp() 函數來加載 WSGI Application,至於如何加載則由 Paste 的配置文件 keystone-paste.ini 來決定,這個文件也是看懂整個程序的關鍵。
        #config.find_paste_config() 用來查找並加載需要用到的 Paste 配置文件,這個文件在源碼中的路徑是 keystone/etc/keystone-paste.ini 文件。

             _unused, application = common.setup_backends(
                 startup_application_fn=loadapp)

             ...
             return application     #返回一個可調用的 application 對象

name很關鍵
name = os.path.basename(__file__) 這個變量從 keystone/httpd/keystone.py 文件傳遞到 initialize_application() 函數,再被傳遞到 keystone_service.loadapp() 函數,最終被傳遞到 paste.deploy.loadapp() 函數。name 是用來確定Application 入口的,表示了配置文件 paste.ini 中一個 composite Section 的名字,指定這個 Section 作為處理 HTTP Request 的第一站。在 Keystone 的 paste.ini 中,請求必須先由 [composite:main] 或者 [composite:admin] 處理,所以在 keystone 項目中,name 的值必須是 main 或者 admin 。

上麵提到的 keystone/httpd/keystone.py 文件中,name 等於文件名的 basename,所以實際部署中,必須把 keystone.py 重命名為 main.py 或者 admin.py ,這樣os.path.basename(__file__)的值才能為 main 或 admin 。

eventlet 部署方式的入口
keystone-all 指令則是采用 eventlet 來進行部署時,可以從 setup.cfg 文件中確定 keystone-all 指令的入口。

#keystone/setup.cfg

[entry_points]
console_scripts
    keystone-all = keystone.cmd.all:main  #表示 keystone-all 的入口是 all.py 文件中的 main() 函數
    keystone-manage = keystone.cmd.manage:main

main() 函數的內容:

#keystone/keystone/cmd/all.py

def main():
    eventlet_server.run(possible_topdir)
    #main() 函數的主要作用就是運行 eventlet_server ,配置文件從 possible_topdir 中給出。

Paste和PasteDeploy

配置文件 paste.ini

使用Paste和PasteDeploy模塊來實現 WSGI Services 時,都需要加載一個 paste.ini 文件。這個文件也是 Paste 模塊的精髓,那麼這個文件如何閱讀呢 ?

paste.ini 文件的格式類似於INI格式,每個 Section 的格式均為 [type:name]
這裏重要的是理解幾種不同 type 的 Section 的作用:

  • composite : 這種 Section 用於將 HTTP URL Request 分發到指定的 Application 。
    Keystone 的 paste.ini 文件中有兩個 composite 的 Section:
#keystone/etc/keystone-paste.ini

[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

#use 是一個關鍵字,指定處理請求的代碼,表明了具體處理請求的分發方式。
#egg:Paste#urlmap 表示使用 Paste 包中的 urlmap 模塊來分發請求。
#/v2.0 /v3 / , 是 urlmap 進行分發時,需要使用到的參數。

注意:在 virtualenv 環境下,是到文件 /lib/python2.7/site-packages/Paste-2.0.2.dist-info/metadata.json 下去尋找 urlmap 關鍵字所對應的函數,而非 egg-info :

{
    ...
    "extensions": {
        ...
        "python.exports": {
            "paste.composite_factory": {
                "cascade": "paste.cascade:make_cascade",
                "urlmap": "paste.urlmap:urlmap_factory"  
            },
    ...
}
# 在這個 JSON 文件中,你可以找到 urlmap 關鍵字對應的 paste.urlmap:urlmap_factory 函數(也就是 paste/urlmap.py 文件中的urlmap_factory()函數)。
# composite 中其他的關鍵字(/v2.0、 /v3、 /)則是 urlmap_factory() 函數的參數,用於表示不同的URL_Path前綴。
# urlmap_factory() 函數會返回一個 WSGI Application, 其功能是根據不同的 URL_Path 前綴,把請求路由給不同的 Application 。

注意:在不同的版本中可能會有不同的 composite section 實現,EXAMPLE-M版:

[composite:rootapp]
paste.composite_factory = glance.api:root_app_factory
/: apiversions
/v1: apiv1app
/v2: apiv2app
/v3: apiv3app

以[composite:main]為例,看看其他關鍵字的作用

[composite:main]
use = egg:Paste#urlmap
/v2. = public_api       # /v2.0 開頭的請求會路由給 public_api 處理
/v3 = api_v3             # /v3 開頭的請求會路由給 api_v3 處理
/ = public_version_api   # / 開頭的請求會路由給 public_version_api 處理
# public_api/api_v3/public_version_api 這些路由的對象就是 paste.ini 中的其他 Secion Name,而且Section Type 必須是 app 或者 pipeline。
  • pipeline : 用來把一係列的 filter 過濾器和 app 串起來。
    它隻有一個關鍵字就是 pipeline。EXAMPLE:
[pipeline:public_api]
pipeline = sizelimit url_normalize request_id build_auth_context token_auth admin_token_auth json_body ec2_extension user_crud_extension public_service

我們以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 service_v3

# pipeline 關鍵字指定了很多個名字,這些名字也是 paste.ini 文件中其他的 section Name。 
# HTTP Request 從 Sectin composite 被轉發到 Section pipeline 之後,該請求會從第一個的 section 開始處理,一直向後傳遞知道結束。
# pipeline 指定的 section 有如下要求:
#   1. 最後一個名字對應的 section 一定要是一個 app 類型的 Section。
#   2. 非最後一個名字對應的 section 一定要是一個 filter 類型的 Section。
  • filter: 實現一個過濾器中間件。
    filter(以WSGI中間件的方式實現)是用來過濾請求和響應的,應該不同的中間件的過濾,該請求就具有了不同的功能。EXAMPLE:
[filter:sizelimit]
paste.filter_factory = oslo_middleware.sizelimit:RequestBodySizeLimiter.factory   
#paste.filter_factory 表示調用哪個函數來獲得這個 filter 中間件。
#這個是 [pipeline:api_v3] 這個 Section 指定的第一個 filter ,作用是限製請求的大小。
  • app: 一個 app 就是一個實現主要功能的具體的 WSGI Application 。
[app:service_v3] #是 [pipeline:api_v3] 這個 pipeline Section 的最後一個元素,必須是一個 app Type。
paste.app_factory = keystone.service:v3_app_factory  

#keystone.service:v3_app_factory 表示獲取哪一個 Application
#paste.app_factory 表示調用哪個函數來獲得 Application 

paste.ini 配置文件中這一大堆配置的作用就是把我們用 Python 寫的 WSGI Application 和 Middleware 串起來,規定好 HTTP Request 處理的路徑。當 Paste + PasteDeploy 模塊提供的 WSGI Server(Web Server) 接受到 URL_Path 形式的 HTTP Request 時,這些 Request 首先會被 Paste 按照配置文件 paste.ini 進行處理,處理的過程為:composite(將Request轉發到pipeline或app) ==> pipeline(包含了filter和app) ==> filter(調用Middleware對Request進行過濾) ==> app(具體的Application來實現Request的操作) 。這個過程就是將 Application 和 Middleware 串起來的過程 。

舉個例子:
一般情況下,如果希望從 Keystone service 獲取一個 token 時,會使到 https://hostname:35357/v3/auth/tokens 這個 API。
我們根據 Keystone 的 paste.ini 配置文件來說明這個 API 是如何被處理的:

  • Step1. (hostname:35357): 這一部分是由 Apache Web Server 來獲取並處理的。然後,請求會被 Web Server 轉到 WSGI Application 的入口,也就是 keystone/httpd/keystone.py 中的 application 對象取處理。

  • Step2. (/v3/auth/tokens): application 對象根據 paste.ini 中的配置來對剩下的(/v3/auth/tokens)部分進行處理。首先請求的 Post=35357 決定了會經過 [composite:admin] section 。(一般是admin監聽35357端口,main監聽5000端口,也會受到 name 變量的影響)

  • Step3. (/v3): 決定將請求路由到哪一個 pipeline secion,[composite:admin] 發現請求的 URL_Path 是 /v3 開頭的,於是就把請求轉發給[pipeline:api_v3]處理,轉發之前,會把 /v3 這個部分去掉。
  • Step4. (/auth/tokens) : [pipeline:api_v3]收到請求,URL_Path是 (/auth/tokens),然後開始調用各個 filter(中間件) 來處理請求。最後會把請求交給[app:service_v3]進行處理。
  • Step5. (/auth/tokens)[app:service_v3] 收到請求,URL_Path是 (/auth/tokens),最後交由的 WSGI Application 去處理。

到此為止,paste.ini 中的配置的所有工作都已經做完了。下麵請求就要轉移到最終的 Application 內部去處理了。

注意:那麼通過 paste.ini 處理過後,剩下的一部分URL_Path(/auth/tokens)的路由還沒確定,它又是怎麼被處理的呢?

中間件的實現

Middleware 中間件在 paste.ini 配置文件中以 filter section 的形式被表示,EXAMPLE

[filter:build_auth_context]
paste.filter_factory = keystone.middleware:AuthContextMiddleware.factory

build_auth_context 這個 filter (中間件)的作用是在 WSGI Application 需要接受的 environ 環境變量參數中添加 KEYSTONE_AUTH_CONTEXT 這個成員Key,包含的內容是認證信息的上下文。這個 filter 的類繼承關係為:

keystone.middleware.core.AuthContextMiddleware
  -> keystone.common.wsgi.Middleware
    -> keystone.common.wsgi.Application
      -> keystone.common.wsgi.BaseApplication
  • class keystone.common.wsgi.Middleware 實現了__call__()方法,是 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 Module 的裝飾器 `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

#process_request() 會根據功能設計創建 auth_context, 然後賦值給 request.environ[authorization.AUTH_CONTEXT_ENV], 這樣就能通過 application 對象的 environ 參數傳遞到WSGI Application中去了。

Routes

對於URL_Path是以 /v3 開頭的請求,在 paste.ini 中會被路由到 [app:service_v3] 這個 app section,並且交給 keystone.service:v3_app_factory這個函數生成的 application 處理。最後這個 application 需要根據 URL_Path 中剩下的部分(/auth/tokens),來實現 URL 路由。這需要使用 Routes 模塊

Routes Module:是用 Python 實現的類似 Rails 的 URL 路由係統,它的主要功能就是把接收到的 URL_Path 映射到對應的操作。Routes 的一般用法是創建一個 Mapper 對象,然後調用該 Mapper 對象的 connect() 方法把 URL_Path 和 HTTP 內建方法映射到一個 Controller 的某個 Action 上。
這裏 Controller 是一個自定義的類實例,每一個資源都對應一個 Controller,是對資源操作方法的集合。Action 表示 Controller 對象提供的操作方法(EG. index/show/delect/update/create )。一般調用這些 Action 的時候會把這個 Action 映射到 HTTP 的內置方法中去。EG. GET/POST/DELETE/PUT 。

EXAMPLE

#keystone/auth/routers.py
class Routers(wsgi.RoutersBase):

    def append_v3_routers(self, mapper, routers):
        auth_controller = controllers.Auth()

        self._add_resource(                     # 2.
            mapper, auth_controller,            # 3. 
            path='/auth/tokens',                # 1.
            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'))

    ...
#1. ._add_resource() 是專為了路由 URL_Path (/auth/tokens) 而定義的。 其他不同的 URL_Path 也會有對應的的方法定義。 方法名是一致的,但參數不同,尤其是參數 path 的值。
#2. _add_resource() 批量為 /auth/tokens 這個 URL_Path 添加多個 HTTP 內建方法的處理函數。
#3. _add_resource() 的其他參數(get_action/head_action/...) ,可以從名字看出這些參數的作用是指定 HTTP 內建方法對應的處理函數,
#    EG. HTTP:GET() ==> Action:get_action ==>  處理函數: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:             #如果傳遞了 get_action 參數,則執行代碼塊
        getattr(controller, get_action)  # ensure the attribute exists
        mapper.connect(path, controller=controller, action=get_action,
                       conditions=dict(method=['GET']))           
        #調用 mapper 對象的 connect 方法指定一個 URL_Path 的 Action:get_action 映射到 HTTP:GET()  
    ...

總結:URL_Path(/auth/tokens) 和 HTTP 內建的方法,在 Routes Module 中被 Mapper 對象的 connect 方法映射到某一個 Controller 的 Action 操作函數中。實現了調用 URL_Path 即調用 Action 操作函數的效果,而且因為 HTTP 的內建方法也被映射在其中,所以可以很容易的使用 HTTP 協議來實現操作。

WebOb

WebOb 有兩個重要的對象:

  • 一個是 Webob.Request,對 WSGI Request 的 environ 參數進行封裝。
  • 一個是 webob.Response ,包含了標準 WSGI Response 的所有元素。
  • 此外,還有一個 webob.exc,針對 HTTP 錯誤代碼進行封裝。

除了這三種對象,WebOb還提供了一個裝飾器webob.dec.wsgify,以便我們可以不使用原始的 WSGI 參數傳遞和返回格式,而全部使用 WebOb 替代。

EXAMPLE:

@wsgify
def myfunc(req):
    return webob.Response('Hey!')

原始方式調用

app_iter = myfunc(environ, start_response)

WebOb調用方式

resp = myfunc(req)

最後更新:2017-05-31 20:01:45

  上一篇:go  阿裏雲新零售解決方案
  下一篇:go  人工智能在媒體領域的研究和應用