? ? ? ?nova-api公布api服務沒實用到一個些框架,基本都是從頭寫的。在不了解它時,以為它很復雜,難以掌握。花了兩三天的時間把它分析一遍后,發現它本身的結構比較簡單,主要難點在于對它所使用的一些類庫不了解,如paste.deploy/webob/routes。對于paste.deploy,結合它的官網文檔把它的源代碼看了兩遍。webob看的是源代碼。routes看的是文檔。對于這些類庫提供的函數,假設從文檔中去理解他們想要做什么,真不是件easy的事。查看事實上現源代碼,就明了了。只是在分析源代碼過程中,碰到每個類庫都去翻一遍它的源代碼,這也是很累人的,后期甚至都不想再看下去了,由于腦子比較厭煩了。所以在學習routes時主要是看它的文檔,基本理解了。
paste.deploy
用來解析/etc/nova/api-paste.ini文件,載入用于服務的wsgi app。它的功能有:
- ?api-paste.ini中配置多個wsgi app,deploy可依據傳入的app name載入指定的wsgi app;
deploy.loadapp("config:/etc/nova/api-paste.ini", name="osapi-compute")載入api-paste.ini中,名為osapi-compute的WSGI APP,并作為結果返回。
- 通過寫入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,使用就更簡單了。
- 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主要提供三種功能。
- 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推斷和檢測的功能。
- Response。該類用于對HTTP的返回值進行封裝。與Request對象類似,相同使用了property簡化對http參數的訪問和設置。支持wsgi app一次返回status和body,這樣更直觀。事實上Response實例本身也是一個wsgi app。
- 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>)? ? ? 這里利用了routes.mapper支持restful api的特性,僅用兩條指令,就定義了十多個url到函數的映射。當然你能夠例如以下方式加入接口,只是代碼稍多,風格不那么統一:
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實現的功能非常easy:依據配置將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_v2 keystone_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.factoryfilter在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)? ? ? ?Middleware初始化接收一個wsgi app,在調用wsgi app之前,運行process_request()對請求進行預處理,推斷請求是否交給傳入的wsgi app,還是直接返回,或者改動些req再給傳入的wsgi app處理。wsgi app返回的response再交給process_response()處理。比如,對于進行驗證的邏輯,能夠放在process_request中,假設驗證通過則繼續交給app處理,否則返回“Authentication required”。只是查看nova全部Mddlerware的編寫,似乎都不用這樣的定義好的結構,而是把處理邏輯都放到__call__中,這樣導致__call__變得復雜,代碼不夠整潔。對于FaultWrapper尚可理解,畢竟須要捕獲wsgi app處理異常嘛,但其他的Middleware就不應該了。這可能是不同程序猿寫,規范就忽略了。
? ? ? 當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')) ......? ? ? ?APIRouter通過調用routes.Mapper.resource()函數建立RESTFUL API,也能夠通過routes.Mapper.connect()來建立url與controller的映射。如上所看到的,servers相關請求的controller設為servers.create_resource(ext_mgr),該函數返回的是一個用nova.api.openstack.compute.servers.Controller()作為初始化參數的nova.api.openstack.wsgi.Resource實例,ips相關請求的controller設為由nova.api.openstack.ips.Controller()初始化的nova.api.openstack.wsgi.Resource實例。由于調用mapper.resource建立ips的url映射時,加入了一個parent_resource參數,使得請求ips相關api的url形式為/v2/project_id/servers/server_id/ips。對于limits、flavors、metadata等請求情況類似。當osapi_compute_app_v2接收到HTTP請求時,將調用nova.wsgi.Router.__call__,它的定義例如以下:
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? ? ? ? 這里開始讓我迷惑了一下,__call__()怎么能返回一個wsgi app呢,直接返回wsgi app那它又怎么被調用呢?查看一下wsgify源代碼,發現假設函數返回的是wsgi app時,它還會被繼續調用,并返回它的處理結果。所以它會繼續調用self._router,_router是routes.middleware.RoutesMiddleware的實例,使用self._dispatch和self.map來初始化的,self.map是在Router的子類nova.api.openstack.APIMapper.__init__中,被初始化為ProjectMapper實例,并調用_setup_routes建立好url與cotroller之間的映射。routes.middleware.RoutesMiddleware.__call__調用mapper.routematch來獲取該url映射的controller等參數,以{"controller":Resource(Controller()), "action": funcname, "project_id": uuid, ...}的格式放在match中。并設置例如以下的environ變量,方便后面調用的self._dispatch訪問。最后調用self._dispatch。
environ['wsgiorg.routing_args'] = ((url), match) environ['routes.route'] = route environ['routes.url'] = url? ? ? ? _dispatch詳細負責url到controller的映射,它通過前面設置environ['wsgiorg.routing_args']來找到url相應的controller。這里的controller就是通過_setup_resource函數設置的controller,及使用響應Controller初始化的Resource實例。Resource通過environ['wsgiorg.routing_args']獲取上面設置的match,該match有一個action屬性,它指定了全部調用Crotroller成員函數的名子,以及其他相關的調用參數。在我們定義Controller的成員函數時,一般須要通過nova.api.openstack.wsgi.{serializers, deserializers}來指定解釋body內容的模板,能夠是xml或者json格式的。前面說過重定義nova.api.openstack.urlmap.URLMap的目的是為了推斷content_type。Resource在解析body時會參考content_type,然后調用響應的解析器進行解析(如XMLDeserializer、JSONDeserializer),然后將body update進action_args,使用action_args來調用Controller成員函數,即終于的http請求處理函數。最后將運行結果使用指定的序列化器序列化,并返回結果。
參考文獻:
[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
更多文章、技術交流、商務合作、聯系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號聯系: 360901061
您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點擊下面給點支持吧,站長非常感激您!手機微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對您有幫助就好】元
