經過demo學習OpenStack開發所需的基礎知識 -- API服務(2)

本文會重點講解OpenStack中使用的API開發框架的使用。可是本文的目的並非覆蓋這些框架的使用細節,而是經過說明重要的部分,下降初學者的入門的門檻。框架的使用細節均可以從文檔中找到。說明一下,除非特殊說明,本文中的相對路徑都是相對於項目源碼目錄的相對路徑python

Paste + PasteDeploy + Routes + WebOb

咱們在API服務(1)中已經提到了,這個框架只在早期開始的項目中使用,新的項目都已經轉到Pecan框架了。可是,早期的項目都是比較核心的項目,所以咱們仍是要學會如何使用這個框架。咱們會以Keystone項目爲例,來講明如何閱讀使用這個框架的開發的API代碼。web

重點在於肯定URL路由

RESTful API程序的主要特色就是URL path會和功能對應起來。這點從API文檔就能夠看得出來,好比用戶管理的功能通常都放在/user這個路徑下。所以,看一個RESTful API程序,通常都是看它實現了哪些URL path,以及每一個path對應了什麼功能,這個通常都是由框架的URL路由功能負責的。因此,熟悉一個RESTful API程序的重點在於肯定URL路由。本章所說的這個框架對於初學者的難點也是如何肯定URL路由。json

WSGI入口和中間件

做爲基礎知識,你須要先了解一下WSGI的相關概念,能夠參考這篇文章WSGI簡介segmentfault

WSGI入口

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

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.initkeystone_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是如何被處理的:

  1. hostname:35357 這一部分是由Web服務器處理的,好比Apache。而後,請求會被轉到WSGI的入口,也就是httpd/keystone.py中的application對象取處理。

  2. application對象是根據paste.ini中的配置來處理的。這裏會先由[composite:admin]來處理(通常是admin監聽35357端口,main監聽5000端口)。

  3. [composite:admin]發現請求的path是/v3開頭的,因而就把請求轉發給[pipeline:api_v3]去處理,轉發以前,會把/v3這個部分去掉。

  4. [pipeline:api_v3]收到請求,path是/auth/tokens,而後開始調用各個filter來處理請求。最後會把請求交給[app:service_v3]進行處理。

  5. [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.Requestwebob.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.pyrouters.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中的各類庫,這些庫的相關文檔均可以在項目官網找到:http://pythonpaste.org/

另外,routes庫的項目官網是:https://routes.readthedocs.org/en/latest/

相關文章
相關標籤/搜索