以下內容轉自 http://www.netfoucs.com/article/xuriwuyun/87993.html
nova-api發佈api服務沒有用到一個些框架,基本都是從頭寫的。在不瞭解它時,以爲它非常複雜,難以掌握。花了兩三天的時間把它分析一遍後,發現它本身的結構比較簡單,主要難點在於對它所使用的一些類庫不瞭解,如paste.deploy/webob/routes。對於paste.deploy,結合它的官網文檔把它的源碼看了兩遍。webob看的是源碼。routes看的是文檔。對於這些類庫提供的函數,如果從文檔中去理解他們想要做什麼,真不是件容易的事。查看其實現源碼,就明瞭了。不過在分析源碼過程中,碰到每一個類庫都去翻一遍它的源碼,這也是非常累人的,後期甚至都不想再看下去了,因爲腦子比較厭煩了。所以在學習routes時主要是看它的文檔,基本理解了。
paste.deploy
用來解析/etc/nova/api-paste.ini文件,加載用於服務的wsgi app。它的功能有:
1 api-paste.ini中配置多個wsgi app,deploy可根據傳入的app name加載指定的wsgi app;
- deploy.loadapp("config:/etc/nova/api-paste.ini", name="osapi-compute")
2 通過寫入api-paste.ini的配置,可方便地實現特定字符開始的url到特定wsgi app的映射。如:
[composite:osapi_compute] use = call:nova.api.openstack.urlmap:urlmap_factory /: oscomputeversions /v2: openstack_compute_api_v2
通過該配置,以“/v2”開始的url將交給名爲openstack_compute_api_v2的WSGI APP處理,其它以“/”開的url就交給oscomputerversions處理。其實這並非deploy的功能,而是上面配置的urlmap實現的。不過通過配置文件,再由deploy調用urlmap,使用就更簡單了。
3 middle ware的簡單加載和去除。
[composite:openstack_compute_api_v2] use = call:nova.api.auth:pipeline_factory keystone = faultwrap sizelimit authtoken keystonecontext ratelimit osapi_compute_app_v2
上面的faultwrap sizelimit authtoken keystonecontext ratelimit都爲middle ware(在nova代碼中稱爲MiddleWare,deploy中稱爲filter),osapi_compute_app_v2纔是wsgi app。請求先交給middle ware處理,再由middle
ware決定要不要或者怎麼交給wsgi app處理。這些middle ware是可以添加和去除的,如果不想對osapi_compute_app_v2進行限速,那麼去掉ratelimit就可以了。其實這是pipeline_factory實現的功能,不過通過deploy來配置加載更方便。
nova-api中提供了很多服務API:ec2(與EC2兼容的API),osapi_compute(openstack compute自己風格的API),osapi_volume(openstack volume服務API),metadata(metadata 服務API)等。通過配置文件api-paste.ini,可以方便管理這些API。
deploy提供了server、filter、app的概念,其中filter(即middle ware)、app在nova-api被重度使用,的確太好用了。發佈每個API時,它們有時需要一些相同的功能,如:keystone驗證、限速、錯誤處理等功能。nova將這些功能實現爲middle ware,如果需要,通過api-paste.ini配置,給它加上就可以。比如,我需要記錄每個訪問nova-api的ip,及它們的訪問次數和訪問的時間。那麼我實現一個middle ware--nova.api.middleware:MonitorMiddleware來記錄這些數據。通過下面的api-paste.ini配置,就可對nova-api的訪問進行記錄了。
[composite:openstack_compute_api_v2] use = call:nova.api.auth:pipeline_factory keystone = faultwrap sizelimit authtoken keystonecontext ratelimit <strong>monitor</strong> osapi_compute_app_v2[filter:<strong>monitor</strong>] paste.filter_factory = nova.api.middleware:MonitorMiddleware.factory
webob
用於對wsgi app進行封裝,簡化wsgi app的定義與編寫。webob主要提供三種功能。
1 Request。該類用於對傳給wsgi app的env參數進行封裝,簡化對HTTP請求參數的訪問和設置。這種簡化體現在(假設用env對Request實例化了一個對象req):
1) 使用間接明瞭的屬性名對HTTP參數進行訪問,req.method可獲取HTTP請求方法(替代REQUEST_METHOD);req.scheme可獲取HTTP請求協議http or https(替代wsgi.url_scheme);req.body獲取請求body(替代wsgi.input)。
2)大量使用property,省去繁瑣細節,簡化HTTP參數的訪問和設置。req.body直接訪問HTTP請求的body,而不用考慮(body的長度和字符編碼);req.POST以字典形式返回POST請求的參數;req.GET以字典形式返回GET請求的參數。
nova.api.openstack.wsgi.Request繼承該類,並加了一個緩存已訪問db的記錄的功能,和對content_type判斷和檢測的功能。
2 Response。該類用於對HTTP的返回值進行封裝。與Request對象類似,同樣使用了property簡化對http參數的訪問和設置。支持wsgi app一次返回status和body,這樣更直觀。其實Response實例本身也是一個wsgi app。
3 decorator--wsgify,裝飾wsgi app,使其可以以如下方式定義:
- @webob.dec.wsgify def wsgi_app(req): #do something with req return req.Response(...)
其中參數req是一個Request(默認)或其子類(通過wsgify(RequestClass=XXX)指定)的實例,是用env初始化的。req.Response默認爲webob.Response。以該種方式定義的wsgi app,其結果可以以三種形式返回:
1)返回一個字符串。wsgify將其作爲body,並加上一些默認的參數,如status=“200 OK", content_type, content_length等,構造成一個HTTP響應結果並返回;
2)返回一個Response實例,直接返回該resp代表的HTTP請求結果;
3)返回一個wsgi app,wsgify會繼續調用該app,並返回app的響應結果。
nova.wsgi.Router就是用第三種返回形式,兩次返回wsgi app,最終將HTTP請求根據url映射到對應的controller處理。
routes
用來給服務內部定義url到具體函數的映射。deploy也有url到服務映射功能,但這個映射層次偏高一點。根據上面配置,deploy將以“/v2”開始的url將交給名爲openstack_compute_api_v2處理。但openstack_compute_api_v2怎麼將/v2/project_id/servers/的GET請求交給nova.api.openstack.compute.servers.Controller.index()處理,並且將POST請求交給create()處理呢;怎麼將/v2/project_id/servers/id的GET請求交給show()處理呢?這個就是routes.mappers所提供的功能,它根據path和請求方法,將請求映射到具體的函數上。如在nova中,添加/v2/project_id/servers/{list_vm_state, os_vmsum}兩個GET請求來分別獲取指定VM的狀態和VM的總數。可在nova.api.openstack.compute.APIRouter中添加如下兩行,將請求分別交給list_vm_state和os_vmsum兩個函數處理並返回結果:
- self.resources['servers'] = servers.create_resource(ext_mgr) mapper.resource("server", "servers", controller=self.resources['servers'], <strong>collection={'list_vm_state': 'GET', 'os_vmsum': 'GET'}</strong>)
- mapper.connect("server", "/{project_id}/servers/list_vm_state", controller=self.resources['servers'], action='list_vm_state', conditions={'list_vm_state': 'GET'}) mapper.connect("server", "/{project_id}/servers/os_vmsum", controller=self.resources['servers'], action='os_vmsum', conditions={'os_vmsum': 'GET'})
主題--nova-api服務流程分析
上面介紹了nova-api發佈所用到的一些lib庫,有了上面的基礎知識,再來分析nova-api的發佈流量,就比較輕鬆了。
nova-api可以提供多種api服務:ec2, osapi_compute, osapi_volume, metadata。可以通過配置項enabled_apis來設置啓動哪些服務,默認情況下,四種服務都是啓動的。
從nova-api的可執行腳本中,可以看出每個nova-api服務都是通過nova.service.WSGIService來管理的:
- class WSGIService(object): def __init__(self, name, loader=None): self.name = name self.manager = self._get_manager() self.loader = loader or wsgi.Loader() self.app = self.loader.load_app(name) self.host = getattr(FLAGS, '%s_listen' % name, "0.0.0.0") self.port = getattr(FLAGS, '%s_listen_port' % name, 0) self.workers = getattr(FLAGS, '%s_workers' % name, None) self.server = wsgi.Server(name, #這裏通過eventlet來啓動服務 self.app, host=self.host, port=self.port) def start(self): if self.manager: self.manager.init_host() self.server.start() ......
從上可知,WSGIService使用self.app = self.loader.load_app(name)來加載wsgi app,app加載完成後,使用nova.wsgi.Server來發布服務。Server首先用指定ip和port實例化一個監聽socket,並使用wsgi.server以協程的方式來發布socket,並將監聽到的http請求交給app處理。對於Server的啓動過程,代碼上理解還是比較簡單的,沒多少分析的。下面我們主要來分析處理HTTP請求的wsgi app是如何構建的,對於每一個請求,它是如何根據url和請求方法將請求分發到具體的具體函數處理的。
上個語句self.loader.load_app(name)中的loader是nova.wsgi.Loader的實例。Loader.load_app(name)執行下面指令,使用deploy來加載wsgi app:
- deploy.loadapp("config:%s" % self.config_path, name=name)
self.config_path爲api-paste.ini文件路徑,一般爲/etc/nova/api-paste.ini。name爲ec2, osapi_compute, osapi_volume, metadata之一,根據指定的name不同來加載不同的wsgi app。下面以name=“osapi_compute”時,加載提供openstack compute API服務的wsgi app作爲具體分析。osapi_compute的配置如下
[composite:osapi_compute] use = call:nova.api.openstack.urlmap:urlmap_factory /: oscomputeversions /v2: openstack_compute_api_v2
osapi_compute是調用urlmap_factory函數返回的一個nova.api.openstack.urlmap.URLMap實例,nova.api.openstack.urlmap.URLMap繼承paste.urlmap.URLMap,它提供了wsgi調用接口,所以該實例爲wsgi app。但是函數nova.api.openstack.urlmap:urlmap_factory與paste.urlmap.urlmap_factory定義完全一樣,不過由於它們所在的module不同,使得它們所用的URLMap分別爲與它處於同一module的URLMap。paste.urlmap.urlmap_factory咋不支持一個傳參,來指定URLMap呢?這樣nova就不用重寫一樣的urlmap_factory了。paste.urlmap.URLMap實現的功能很簡單:根據配置將url映射到特定wsgi app,並根據url的長短作一個優先級排序,url較長的將優先進行匹配。所以/v2將先於/進行匹配。URLMap在調用下層的wsgi app前,會更新SCRIPT_NAME和PATH_INFO。nova.api.openstack.urlmap.URLMap繼承了paste.urlmap.URLMap,並寫了一堆代碼,其實只是爲了實現對請求類型的判斷,並設置environ['nova.best_content_type']:如果url的後綴名爲json(如/xxxx.json),那麼environ['nova.best_content_type']=“application/json”。如果url沒有後綴名,那麼將通過HTTP headers的content_type字段中mimetype判斷。否則默認environ['nova.best_content_type']=“application/json”。
經上面配置加載的osapi_compute爲一個URLMap實例,wsgi server的接受的HTTP請求將直接交給該實例處理。它將url爲'/v2/.*'的請求將交給openstack_compute_api_v2,url爲'/'的請求交給oscomputerversions處理(它直接返回系統版本號)。其它的url請求,則返回NotFound。下面繼續分析openstack_compute_api_v2,其配置如下:
[composite:openstack_compute_api_v2] use = call:nova.api.auth:pipeline_factory noauth = faultwrap sizelimit noauth ratelimit osapi_compute_app_v2 keystone = faultwrap sizelimit authtoken keystonecontext ratelimit osapi_compute_app_v2keystone_nolimit = faultwrap sizelimit authtoken keystonecontext osapi_compute_app_v2
openstack_compute_api_v2是調用nova.api.auth.pipeline_factory()返回的wsgi app。pipeline_factory()根據配置項auth_strategy來加載不同的filter和最終的osapi_compute_app_v2。filter的大概配置如下:
[filter:faultwrap] paste.filter_factory = nova.api.openstack:FaultWrapper.factory
filter在nova中對應的是nova.wsgi.Middleware,它的定義如下:
- class Middleware(Application): @classmethod def factory(cls, global_config, **local_config): def _factory(app): return cls(app, **local_config) return _factory def __init__(self, application): self.application = application def process_request(self, req): return None def process_response(self, response): return response @webob.dec.wsgify(RequestClass=Request) def __call__(self, req): response = self.process_request(req) if response: return response response = req.get_response(self.application) return self.process_response(response)
當auth_strategy=“keystone”時,openstack_compute_api_v2=FaultWrapper(RequestBodySizeLimiter(auth_token(NovaKeystoneContext(RateLimitingMiddleware(osapi_compute_app_v2)))))。所以HTTP請求需要經過五個Middleware的處理,才能到達osapi_compute_app_v2。這五個Middleware分別完成:
1)異常捕獲,防止服務內部處理異常導致wsgi server掛掉;
2)限制HTTP請求body大小,對於太大的body,將直接返回BadRequest;
3)對請求keystone對header中token id進行驗證;
4)利用headers初始化一個nova.context.RequestContext實例,並賦給req.environ['nova.context'];
5)限制用戶的訪問速度。
當HTTP請經過上面五個Middlerware處理後,最終交給osapi_compute_app_v2,它是怎麼繼續處理呢?它的配置如下:
[app:osapi_compute_app_v2] paste.app_factory = nova.api.openstack.compute:APIRouter.factory
osapi_compute_app_v2是調用nova.api.openstack.compute.APIRouter.factory()返回的一個APIRouter實例。nova.api.openstack.compute.APIRouter繼承nova.api.openstack.APIRouter,nova.api.openstack.APIRouter又繼承nova.wsgi.APIRouter。APIRouter通過A它的成員變量mapper來建立和維護url與controller之間的映射,該mapper是nova.api.openstack.ProjectMapper的實例,它繼承nova.api.openstack.APIMapper(routes.Mapper)。APIMapper將每個url的format限制爲json或xml,對於其它擴展名的url將返回NotFound。ProjectMapper在每個請求url前面加上一個project_id,這樣每個請求的url都需要帶上用戶所屬的project
id,所以一般請求的url爲/v2/project_id/resources。nova.api.openstack.compute.APIRouter.setup_routes代碼如下:
- class APIRouter(nova.api.openstack.APIRouter): ExtensionManager = extensions.ExtensionManager def _setup_routes(self, mapper, ext_mgr): self.resources['servers'] = servers.create_resource(ext_mgr) mapper.resource("server", "servers", controller=self.resources['servers']) self.resources['ips'] = ips.create_resource() mapper.resource("ip", "ips", controller=self.resources['ips'], parent_resource=dict(member_name='server', collection_name='servers')) ......
- class Router(object): def __init__(self, mapper): self.map = mapper self._router = routes.middleware.RoutesMiddleware(self._dispatch, self.map) @webob.dec.wsgify(RequestClass=Request) def __call__(self, req): return self._router @staticmethod @webob.dec.wsgify(RequestClass=Request) def _dispatch(req): match = req.environ['wsgiorg.routing_args'][1] if not match: return webob.exc.HTTPNotFound() app = match['controller'] return app
- environ['wsgiorg.routing_args'] = ((url), match) environ['routes.route'] = route environ['routes.url'] = url
參考文獻:
[1] https://docs.python.org/2/library/re.html
[2] http://routes.readthedocs.org/en/latest/restful.html
[3] http://pythonpaste.org/deploy/
[4] https://wiki.python.org/moin/MiniDom