? ? ? ?nova-api公布api服務(wù)沒實(shí)用到一個(gè)些框架,基本都是從頭寫的。在不了解它時(shí),以為它很復(fù)雜,難以掌握。花了兩三天的時(shí)間把它分析一遍后,發(fā)現(xiàn)它本身的結(jié)構(gòu)比較簡單,主要難點(diǎn)在于對(duì)它所使用的一些類庫不了解,如paste.deploy/webob/routes。對(duì)于paste.deploy,結(jié)合它的官網(wǎng)文檔把它的源代碼看了兩遍。webob看的是源代碼。routes看的是文檔。對(duì)于這些類庫提供的函數(shù),假設(shè)從文檔中去理解他們想要做什么,真不是件easy的事。查看事實(shí)上現(xiàn)源代碼,就明了了。只是在分析源代碼過程中,碰到每個(gè)類庫都去翻一遍它的源代碼,這也是很累人的,后期甚至都不想再看下去了,由于腦子比較厭煩了。所以在學(xué)習(xí)routes時(shí)主要是看它的文檔,基本理解了。
paste.deploy
用來解析/etc/nova/api-paste.ini文件,載入用于服務(wù)的wsgi app。它的功能有:
- ?api-paste.ini中配置多個(gè)wsgi app,deploy可依據(jù)傳入的app name載入指定的wsgi app;
deploy.loadapp("config:/etc/nova/api-paste.ini", name="osapi-compute")載入api-paste.ini中,名為osapi-compute的WSGI APP,并作為結(jié)果返回。
- 通過寫入api-paste.ini的配置,可方便地實(shí)現(xiàn)特定字符開始的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處理。事實(shí)上這并不是deploy的功能,而是上面配置的urlmap實(shí)現(xiàn)的。只是通過配置文件,再由deploy調(diào)用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。請(qǐng)求先交給middle ware處理,再由middle ware決定要不要或者怎么交給wsgi app處理。這些middle ware是能夠加入和去除的,假設(shè)不想對(duì)osapi_compute_app_v2進(jìn)行限速,那么去掉ratelimit就能夠了。事實(shí)上這是pipeline_factory實(shí)現(xiàn)的功能,只是通過deploy來配置載入更方便。
? ? ? nova-api中提供了非常多服務(wù)API:ec2(與EC2兼容的API),osapi_compute(openstack compute自己風(fēng)格的API),osapi_volume(openstack volume服務(wù)API),metadata(metadata 服務(wù)API)等。通過配置文件api-paste.ini,能夠方便管理這些API。
? ? ? deploy提供了server、filter、app的概念,當(dāng)中filter(即middle ware)、app在nova-api被重度使用,的確太好用了。公布每一個(gè)API時(shí),它們有時(shí)須要一些同樣的功能,如:keystone驗(yàn)證、限速、錯(cuò)誤處理等功能。nova將這些功能實(shí)現(xiàn)為middle ware,假設(shè)須要,通過api-paste.ini配置,給它加上就能夠。比方,我須要記錄每一個(gè)訪問nova-api的ip,及它們的訪問次數(shù)和訪問的時(shí)間。那么我實(shí)現(xiàn)一個(gè)middle ware--nova.api.middleware:MonitorMiddleware來記錄這些數(shù)據(jù)。通過以下的api-paste.ini配置,就可對(duì)nova-api的訪問進(jìn)行記錄了。
[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
用于對(duì)wsgi app進(jìn)行封裝,簡化wsgi app的定義與編寫。webob主要提供三種功能。
- Request。該類用于對(duì)傳給wsgi app的env參數(shù)進(jìn)行封裝,簡化對(duì)HTTP請(qǐng)求參數(shù)的訪問和設(shè)置。這樣的簡化體如今(如果用env對(duì)Request實(shí)例化了一個(gè)對(duì)象req):
1) 使用間接明了的屬性名對(duì)HTTP參數(shù)進(jìn)行訪問,req.method可獲取HTTP請(qǐng)求方法(替代REQUEST_METHOD);req.scheme可獲取HTTP請(qǐng)求協(xié)議http or https(替代wsgi.url_scheme);req.body獲取請(qǐng)求body(替代wsgi.input)。
2)大量使用property,省去繁瑣細(xì)節(jié),簡化HTTP參數(shù)的訪問和設(shè)置。req.body直接訪問HTTP請(qǐng)求的body,而不用考慮(body的長度和字符編碼);req.POST以字典形式返回POST請(qǐng)求的參數(shù);req.GET以字典形式返回GET請(qǐng)求的參數(shù)。
nova.api.openstack.wsgi.Request繼承該類,并加了一個(gè)緩存已訪問db的記錄的功能,和對(duì)content_type推斷和檢測(cè)的功能。
- Response。該類用于對(duì)HTTP的返回值進(jìn)行封裝。與Request對(duì)象類似,相同使用了property簡化對(duì)http參數(shù)的訪問和設(shè)置。支持wsgi app一次返回status和body,這樣更直觀。事實(shí)上Response實(shí)例本身也是一個(gè)wsgi app。
- decorator--wsgify,裝飾wsgi app,使其能夠以例如以下方式定義:
@webob.dec.wsgify def wsgi_app(req): #do something with req return req.Response(...)
當(dāng)中參數(shù)req是一個(gè)Request(默認(rèn))或其子類(通過wsgify(RequestClass=XXX)指定)的實(shí)例,是用env初始化的。req.Response默覺得webob.Response。以該種方式定義的wsgi app,其結(jié)果能夠以三種形式返回:
1)返回一個(gè)字符串。wsgify將其作為body,并加上一些默認(rèn)的參數(shù),如status=“200 OK", content_type, content_length等,構(gòu)造成一個(gè)HTTP響應(yīng)結(jié)果并返回;
2)返回一個(gè)Response實(shí)例,直接返回該resp代表的HTTP請(qǐng)求結(jié)果;
3)返回一個(gè)wsgi app,wsgify會(huì)繼續(xù)調(diào)用該app,并返回app的響應(yīng)結(jié)果。
nova.wsgi.Router就是用第三種返回形式,兩次返回wsgi app,終于將HTTP請(qǐng)求依據(jù)url映射到相應(yīng)的controller處理。?
routes
? ? ? ?用來給服務(wù)內(nèi)部定義url到詳細(xì)函數(shù)的映射。deploy也有url到服務(wù)映射功能,但這個(gè)映射層次偏高一點(diǎn)。依據(jù)上面配置,deploy將以“/v2”開始的url將交給名為openstack_compute_api_v2處理。但openstack_compute_api_v2怎么將/v2/project_id/servers/的GET請(qǐng)求交給nova.api.openstack.compute.servers.Controller.index()處理,而且將POST請(qǐng)求交給create()處理呢;怎么將/v2/project_id/servers/id的GET請(qǐng)求交給show()處理呢?這個(gè)就是routes.mappers所提供的功能,它依據(jù)path和請(qǐng)求方法,將請(qǐng)求映射到詳細(xì)的函數(shù)上。如在nova中,加入/v2/project_id/servers/{list_vm_state,?os_vmsum}兩個(gè)GET請(qǐng)求來分別獲取指定VM的狀態(tài)和VM的總數(shù)。可在nova.api.openstack.compute.APIRouter中加入例如以下兩行,將請(qǐng)求分別交給list_vm_state和os_vmsum兩個(gè)函數(shù)處理并返回結(jié)果:
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的特性,僅用兩條指令,就定義了十多個(gè)url到函數(shù)的映射。當(dāng)然你能夠例如以下方式加入接口,只是代碼稍多,風(fēng)格不那么統(tǒng)一:
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服務(wù)流程分析
? ? ? ?上面介紹了nova-api公布所用到的一些lib庫,有了上面的基礎(chǔ)知識(shí),再來分析nova-api的公布流量,就比較輕松了。
? ? ? ?nova-api能夠提供多種api服務(wù):ec2, osapi_compute, osapi_volume, metadata。能夠通過配置項(xiàng)enabled_apis來設(shè)置啟動(dòng)哪些服務(wù),默認(rèn)情況下,四種服務(wù)都是啟動(dòng)的。
? ? ? ?從nova-api的可運(yùn)行腳本中,能夠看出每一個(gè)nova-api服務(wù)都是通過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來啟動(dòng)服務(wù) 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來公布服務(wù)。Server首先用指定ip和port實(shí)例化一個(gè)監(jiān)聽socket,并使用wsgi.server以協(xié)程的方式來公布socket,并將監(jiān)聽到的http請(qǐng)求交給app處理。對(duì)于Server的啟動(dòng)過程,代碼上理解還是比較簡單的,沒多少分析的。以下我們主要來分析處理HTTP請(qǐng)求的wsgi app是怎樣構(gòu)建的,對(duì)于每個(gè)請(qǐng)求,它是怎樣依據(jù)url和請(qǐng)求方法將請(qǐng)求分發(fā)到詳細(xì)的詳細(xì)函數(shù)處理的。
? ? ? ?上個(gè)語句self.loader.load_app(name)中的loader是nova.wsgi.Loader的實(shí)例。Loader.load_app(name)運(yùn)行以下指令,使用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之中的一個(gè),依據(jù)指定的name不同來載入不同的wsgi app。以下以name=“osapi_compute”時(shí),載入提供openstack compute API服務(wù)的wsgi app作為詳細(xì)分析。osapi_compute的配置例如以下
[composite:osapi_compute] use = call:nova.api.openstack.urlmap:urlmap_factory /: oscomputeversions /v2: openstack_compute_api_v2
? ? ? ? osapi_compute是調(diào)用urlmap_factory函數(shù)返回的一個(gè)nova.api.openstack.urlmap.URLMap實(shí)例,nova.api.openstack.urlmap.URLMap繼承paste.urlmap.URLMap,它提供了wsgi調(diào)用接口,所以該實(shí)例為wsgi app。可是函數(shù)nova.api.openstack.urlmap:urlmap_factory與paste.urlmap.urlmap_factory定義全然一樣,只是因?yàn)樗鼈兯诘膍odule不同,使得它們所用的URLMap分別為與它處于同一module的URLMap。paste.urlmap.urlmap_factory咋不支持一個(gè)傳參,來指定URLMap呢?這樣nova就不用重寫一樣的urlmap_factory了。paste.urlmap.URLMap實(shí)現(xiàn)的功能非常easy:依據(jù)配置將url映射到特定wsgi app,并依據(jù)url的長短作一個(gè)優(yōu)先級(jí)排序,url較長的將優(yōu)先進(jìn)行匹配。所以/v2將先于/進(jìn)行匹配。URLMap在調(diào)用下層的wsgi app前,會(huì)更新SCRIPT_NAME和PATH_INFO。nova.api.openstack.urlmap.URLMap繼承了paste.urlmap.URLMap,并寫了一堆代碼,事實(shí)上僅僅是為了實(shí)現(xiàn)對(duì)請(qǐng)求類型的推斷,并設(shè)置environ['nova.best_content_type']:假設(shè)url的后綴名為json(如/xxxx.json),那么environ['nova.best_content_type']=“application/json”。假設(shè)url沒有后綴名,那么將通過HTTP headers的content_type字段中mimetype推斷。否則默認(rèn)environ['nova.best_content_type']=“application/json”。
? ? ? ?經(jīng)上面配置載入的osapi_compute為一個(gè)URLMap實(shí)例,wsgi server的接受的HTTP請(qǐng)求將直接交給該實(shí)例處理。它將url為'/v2/.*'的請(qǐng)求將交給openstack_compute_api_v2,url為'/'的請(qǐng)求交給oscomputerversions處理(它直接返回系統(tǒng)版本)。其他的url請(qǐng)求,則返回NotFound。以下繼續(xù)分析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是調(diào)用nova.api.auth.pipeline_factory()返回的wsgi app。pipeline_factory()依據(jù)配置項(xiàng)auth_strategy來載入不同的filter和終于的osapi_compute_app_v2。filter的大概配置例如以下:
[filter:faultwrap] paste.filter_factory = nova.api.openstack:FaultWrapper.factoryfilter在nova中相應(yīng)的是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初始化接收一個(gè)wsgi app,在調(diào)用wsgi app之前,運(yùn)行process_request()對(duì)請(qǐng)求進(jìn)行預(yù)處理,推斷請(qǐng)求是否交給傳入的wsgi app,還是直接返回,或者改動(dòng)些req再給傳入的wsgi app處理。wsgi app返回的response再交給process_response()處理。比如,對(duì)于進(jìn)行驗(yàn)證的邏輯,能夠放在process_request中,假設(shè)驗(yàn)證通過則繼續(xù)交給app處理,否則返回“Authentication required”。只是查看nova全部Mddlerware的編寫,似乎都不用這樣的定義好的結(jié)構(gòu),而是把處理邏輯都放到__call__中,這樣導(dǎo)致__call__變得復(fù)雜,代碼不夠整潔。對(duì)于FaultWrapper尚可理解,畢竟須要捕獲wsgi app處理異常嘛,但其他的Middleware就不應(yīng)該了。這可能是不同程序猿寫,規(guī)范就忽略了。
? ? ? 當(dāng)auth_strategy=“keystone”時(shí),openstack_compute_api_v2=FaultWrapper(RequestBodySizeLimiter(auth_token(NovaKeystoneContext(RateLimitingMiddleware(osapi_compute_app_v2)))))。所以HTTP請(qǐng)求須要經(jīng)過五個(gè)Middleware的處理,才干到達(dá)osapi_compute_app_v2。這五個(gè)Middleware分別完畢:
1)異常捕獲,防止服務(wù)內(nèi)部處理異常導(dǎo)致wsgi server掛掉;
2)限制HTTP請(qǐng)求body大小,對(duì)于太大的body,將直接返回BadRequest;
3)對(duì)請(qǐng)求keystone對(duì)header中token id進(jìn)行驗(yàn)證;
4)利用headers初始化一個(gè)nova.context.RequestContext實(shí)例,并賦給req.environ['nova.context'];
5)限制用戶的訪問速度。
? ? ? 當(dāng)HTTP請(qǐng)經(jīng)過上面五個(gè)Middlerware處理后,終于交給osapi_compute_app_v2,它是怎么繼續(xù)處理呢?它的配置例如以下:
[app:osapi_compute_app_v2] paste.app_factory = nova.api.openstack.compute:APIRouter.factory? ? ? ?osapi_compute_app_v2是調(diào)用nova.api.openstack.compute.APIRouter.factory()返回的一個(gè)APIRouter實(shí)例。nova.api.openstack.compute.APIRouter繼承nova.api.openstack.APIRouter,nova.api.openstack.APIRouter又繼承nova.wsgi.APIRouter。APIRouter通過A它的成員變量mapper來建立和維護(hù)url與controller之間的映射,該mapper是nova.api.openstack.ProjectMapper的實(shí)例,它繼承nova.api.openstack.APIMapper(routes.Mapper)。APIMapper將每一個(gè)url的format限制為json或xml,對(duì)于其他擴(kuò)展名的url將返回NotFound。ProjectMapper在每一個(gè)請(qǐng)求url前面加上一個(gè)project_id,這樣每一個(gè)請(qǐng)求的url都須要帶上用戶所屬的project id,所以一般請(qǐng)求的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通過調(diào)用routes.Mapper.resource()函數(shù)建立RESTFUL API,也能夠通過routes.Mapper.connect()來建立url與controller的映射。如上所看到的,servers相關(guān)請(qǐng)求的controller設(shè)為servers.create_resource(ext_mgr),該函數(shù)返回的是一個(gè)用nova.api.openstack.compute.servers.Controller()作為初始化參數(shù)的nova.api.openstack.wsgi.Resource實(shí)例,ips相關(guān)請(qǐng)求的controller設(shè)為由nova.api.openstack.ips.Controller()初始化的nova.api.openstack.wsgi.Resource實(shí)例。由于調(diào)用mapper.resource建立ips的url映射時(shí),加入了一個(gè)parent_resource參數(shù),使得請(qǐng)求ips相關(guān)api的url形式為/v2/project_id/servers/server_id/ips。對(duì)于limits、flavors、metadata等請(qǐng)求情況類似。當(dāng)osapi_compute_app_v2接收到HTTP請(qǐng)求時(shí),將調(diào)用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__()怎么能返回一個(gè)wsgi app呢,直接返回wsgi app那它又怎么被調(diào)用呢?查看一下wsgify源代碼,發(fā)現(xiàn)假設(shè)函數(shù)返回的是wsgi app時(shí),它還會(huì)被繼續(xù)調(diào)用,并返回它的處理結(jié)果。所以它會(huì)繼續(xù)調(diào)用self._router,_router是routes.middleware.RoutesMiddleware的實(shí)例,使用self._dispatch和self.map來初始化的,self.map是在Router的子類nova.api.openstack.APIMapper.__init__中,被初始化為ProjectMapper實(shí)例,并調(diào)用_setup_routes建立好url與cotroller之間的映射。routes.middleware.RoutesMiddleware.__call__調(diào)用mapper.routematch來獲取該url映射的controller等參數(shù),以{"controller":Resource(Controller()), "action": funcname, "project_id": uuid, ...}的格式放在match中。并設(shè)置例如以下的environ變量,方便后面調(diào)用的self._dispatch訪問。最后調(diào)用self._dispatch。
environ['wsgiorg.routing_args'] = ((url), match) environ['routes.route'] = route environ['routes.url'] = url? ? ? ? _dispatch詳細(xì)負(fù)責(zé)url到controller的映射,它通過前面設(shè)置environ['wsgiorg.routing_args']來找到url相應(yīng)的controller。這里的controller就是通過_setup_resource函數(shù)設(shè)置的controller,及使用響應(yīng)Controller初始化的Resource實(shí)例。Resource通過environ['wsgiorg.routing_args']獲取上面設(shè)置的match,該match有一個(gè)action屬性,它指定了全部調(diào)用Crotroller成員函數(shù)的名子,以及其他相關(guān)的調(diào)用參數(shù)。在我們定義Controller的成員函數(shù)時(shí),一般須要通過nova.api.openstack.wsgi.{serializers, deserializers}來指定解釋body內(nèi)容的模板,能夠是xml或者json格式的。前面說過重定義nova.api.openstack.urlmap.URLMap的目的是為了推斷content_type。Resource在解析body時(shí)會(huì)參考content_type,然后調(diào)用響應(yīng)的解析器進(jìn)行解析(如XMLDeserializer、JSONDeserializer),然后將body update進(jìn)action_args,使用action_args來調(diào)用Controller成員函數(shù),即終于的http請(qǐng)求處理函數(shù)。最后將運(yùn)行結(jié)果使用指定的序列化器序列化,并返回結(jié)果。
參考文獻(xiàn):
[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
更多文章、技術(shù)交流、商務(wù)合作、聯(lián)系博主
微信掃碼或搜索:z360901061

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