在前面的博文中,主要分析了
Glance
及
Nova
相關的代碼,從這篇文章開始我将轉到
Cinder
的源碼分析上來。
Cinder
子產品在
Openstack
中為雲主機提供塊存儲,主要包含:
cinder-api
,
cinder-scheduler
,
cinder-volume
及
cinder-backup
4個部分,後續将通過一系列文章逐個分析各個元件的源碼。
今天先來看看
cinder-api
啟動過程的源碼分析,預計将包括如下幾個方面的内容:
- 請求路由映射(Python Routes)
- WSGI 應用發現(Python Paste Deployment)
- WSGI伺服器
限于篇幅,可能将上述主題拆分到多篇博文,下面一起來看具體内容:
啟動 cinder-api
服務
cinder-api
當你通過
cinder-api
指令(如:
/usr/bin/cinder-api --config-file /etc/cinder/cinder.conf
)啟動
api
服務時,執行的實際上是
cinder/cmd/api.py/main()
函數, 如下:
#`cinder/cmd/api.py/main`
def main():
"""省略次要代碼,完成代碼請檢視相關檔案"""
#加載輔助對象,封裝與資料庫相關的操作
objects.register_all()
#加載配置并設定日志
CONF(sys.argv[:], project='cinder',
version=version.version_string())
logging.setup(CONF, "cinder")
"""初始化rpc:
設定全局Transport和Notifier,Transport是
oslo_messaging/transport.py/Transport執行個體,我采用的是預設的
rpc_backend=rabbit,是以Transport采用的driver=oslo_messaging/
_drivers/impl_rabbit.py/RabbitDriver;Notifier是一個通知消息發
送器,它借助Transport将通知發送發送給ceilometer
"""
rpc.init(CONF)
#通過服務啟動器啟動WSGI服務(`osapi_volume`)并等待服務啟動成功
#在初始化WSGI服務時,會設定路由映射以及加載WSGI應用程式
#在啟動WSGI服務時,會啟動http監聽
#下文具體分析相關内容
launcher = service.process_launcher()
server = service.WSGIService('osapi_volume')
launcher.launch_service(server, workers=server.workers)
launcher.wait()
建立 WSGIService
服務對象
WSGIService
def main():
......
#建立一個名為`osapi_volume`的`WSGIService`服務對象
server = service.WSGIService('osapi_volume')
......
#接上文,一起來看看`WSGIService`服務對象的初始化函數
#`cinder/service.py/WSGIService.__init__`
def __init__(self, name, loader=None):
"""Initialize, but do not start the WSGI server."""
#服務名`osapi_volume`
self.name = name
#加載名為(`osapi_volume_manager`)的管理器(None)
self.manager = self._get_manager()
"""建立WSGI應用加載器(`cinder/wsgi/common.py/Loader`)
并根據配置檔案(`cinder.conf`)設定應用配置路徑:
`config_path` = `/etc/cinder/paste-api.ini`
"""
self.loader = loader or wsgi_common.Loader()
"""加載WSGI應用并設定路由映射
return paste.urlmap.URLMap, 請看後文的具體分析
"""
self.app = self.loader.load_app(name)
"""根據配置檔案(`cinder.conf`)設定監聽位址及工作線程數
如果未指定監聽ip及端口就分别設定為`0.0.0.0`及`0`
如果為指定工作線程數就設定為cpu個數
如果設定的工作線程數小于1,則抛異常
"""
self.host = getattr(CONF, '%s_listen' % name, "0.0.0.0")
self.port = getattr(CONF, '%s_listen_port' % name, )
self.workers = (getattr(CONF, '%s_workers' % name, None) or
processutils.get_worker_count())
if self.workers and self.workers < :
worker_name = '%s_workers' % name
msg = (_("%(worker_name)s value of %(workers)d is"
"invalid, must be greater than 0.") %
{'worker_name': worker_name,
'workers': self.workers})
raise exception.InvalidInput(msg)
"""如果CONF.profiler.profiler_enabled = True就開啟性能分析
建立一個類型為`Messaging`的通知器(`_notifier`),将性能資料發送給
ceilometer
"""
setup_profiler(name, self.host)
#建立WSGI伺服器對象(`cinder/wsgi/eventlet_server.py/Server`)
#下一篇博文再具體分析WSGI伺服器的初始化及啟動過程,敬請期待!!!
self.server = wsgi.Server(name,
self.app,
host=self.host,
port=self.port)
小結:在初始化
WSGIService
服務對象過程中,主要完成了如下操作:
- 加載
(WSGI Application
)Python Paste Deployment
- 設定路由映射(
)Python Routes
- 建立WSGI伺服器對象并完成初始化
先來看
WSGI Application
的加載過程:
加載 WSGI
應用
WSGI
上文的
self.loader.load_app(name)
,執行的是如下的調用:
#`cinder/wsgi/common.py/Loader.load_app`
def load_app(self, name):
"""Return the paste URLMap wrapped WSGI application.
`Python Paste`系統可以用來發現以及配置`WSGI`應用及服務, 包含如下三
種調用入口:
`loadapp` `loadfilter` `loadserver`
| | |
| |
|
V
`loadobj`
|
V
`loadcontext`
|
| |
| | |
V V V
_loadconfig _loadegg _loadfunc
分别用來配置`WSGI App`,`WSGI Filter`,`WSGI Server`;
`loadcontext`方法基于配置檔案類型(`config`,`egg`,`call`),調用具
體的配置方法,在我們的示例中是:`loadapp` -> `loadobj` ->
`loadcontext` -> `_loadconfig`,下文依次分析:
"""
try:
#從`self.config_path`(`/etc/cinder/api-paste.ini`)指定的
#配置中加載名為`name`(`osapi_volume`)的應用
return deploy.loadapp("config:%s" % self.config_path,
name=name)
except LookupError:
LOG.exception(_LE("Error loading app %s"), name)
raise exception.PasteAppNotFound(name=name, path=self.config_path)
#接上文,直接通過`Python Paste`系統配置`WSGI`應用
#`../site-packages/paste/deploy/loadwsgi.py/loadapp`
def loadapp(uri, name=None, **kw):
"""輸入參數如下:
uri: 'config:/etc/cinder/api-paste.ini'
name: 'osapi_volume'
**kw: None
"""
"""APP = _APP(),是一個_APP執行個體對象,定義應用所支援的協定及其字首:
APP.name = 'application'
APP.egg_protocols = [['paste.app_factory'],
['paste.composite_factory'],
['paste.composit_factory']]
APP.config_prefixes = [['app', 'application'],
['composite', 'composit'],
['pipeline],
['filter-app']]
在後文的分析中會根據應用的協定來生成上下文(`context`)
"""
return loadobj(APP, uri, name=name, **kw)
#接上文`loadobj`
def loadobj(object_type, uri, name=None, relative_to=None,
global_conf=None):
"""根據應用的協定類型生成上下文并執行
object_type: _APP對象
uri: 'config:/etc/cinder/api-paste.ini'
name: 'osapi_volume'
"""
context = loadcontext(
object_type, uri, name=name, relative_to=relative_to,
global_conf=global_conf)
return context.create()
#接上文:這是一個工廠方法,它根據uri中的配置檔案類型
#(`config`,`egg`,`call`)分别調用具體的配置方法
#(`_loadconfig`,`_loadegg`, `_loadfunc`)
def loadcontext(object_type, uri, name=None, relative_to=None,
global_conf=None):
"""建立應用上下文,結合輸入參數,代碼邏輯就很好了解了
object_type: _APP對象
uri: 'config:/etc/cinder/api-paste.ini'
name: 'osapi_volume'
relative_to: None
global_conf: None
"""
if '#' in uri:
if name is None:
uri, name = uri.split('#', )
else:
# @@: Ignore fragment or error?
uri = uri.split('#', )[]
if name is None:
name = 'main'
if ':' not in uri:
raise LookupError("URI has no scheme: %r" % uri)
"""分割uri路徑:
scheme = 'config'
path = '/etc/cinder/api-paste.ini'
"""
scheme, path = uri.split(':', )
scheme = scheme.lower()
#_loaders是一個全局變量,包含:'config','egg', 'call'三種配置類型
#方法
if scheme not in _loaders:
raise LookupError(
"URI scheme not known: %r (from %s)"
% (scheme, ', '.join(_loaders.keys())))
"""path: '/etc/cinder/api-paste.ini'
這裡_loaders['config'] = _loadconfig, 請看下文的分析
"""
return _loaders[scheme](
object_type,
uri, path, name=name, relative_to=relative_to,
global_conf=global_conf)
#接上文:_loaders[scheme] = _loadconfig
def _loadconfig(object_type, uri, path, name, relative_to,
global_conf):
"""結合輸入參數,代碼也很好了解;輸入參數如下:
object_type: _APP對象
uri: 'config:/etc/cinder/api-paste.ini'
path: '/etc/cinder/api-paste.ini'
name: 'osapi_volume'
relative_to: None
global_conf: None
"""
isabs = os.path.isabs(path)
# De-Windowsify the paths:
path = path.replace('\\', '/')
if not isabs:
if not relative_to:
raise ValueError(
"Cannot resolve relative uri %r;no relative_to"
"keyword argument given" % uri)
relative_to = relative_to.replace('\\', '/')
if relative_to.endswith('/'):
path = relative_to + path
else:
path = relative_to + '/' + path
if path.startswith('///'):
path = path[:]
path = unquote(path)
"""建立配置加載器ConfigLoader對象,用于加載配置檔案内容,後續所有的
配置解析操作都由該對象完成, 實際上基于不同的`WSGI`程式類型,它分别提
供了相應的調用接口:
`get_app` `get_server` `get_filter`
| | |
V V V
`app_context` `server_context` `filter_context`
| | |
| |
|
V
get_context
|
V
object_type.invoke
看完後文的分析,你應該會更有體會!!!
"""
loader = ConfigLoader(path)
#如果全局配置不為空,更新`loader`的`defaults`屬性
if global_conf:
loader.update_defaults(global_conf, overwrite=False)
#解析配置檔案内容,擷取上下文(`context`),請看下文的分析
return loader.get_context(object_type, name, global_conf)
#接上文:`loader.get_context`
def get_context(self, object_type, name=None,
global_conf=None):
"""建立上下文的主要函數
如果`name`滿足正規表達式:re.compile(r'^[a-zA-Z]+:'),就再次調
用`loadcontext`加載上下文,如果不滿足條件就先解析配置,然後再根據選
項條件進入不同分支做進一步的處理
以`osapi_volume`為例分析,其在`api-paste.ini`中的内容為:
[composite:osapi_volume]
use = call:cinder.api:root_app_factory
/: apiversions
/v1: openstack_volume_api_v1
/v2: openstack_volume_api_v2
首次調用時(序号2)輸入參數:name = `osapi_volume`,不滿足正則條件,
就先通過`find_config_section`方法從配置檔案加載配置段,然後再根據配
置字首(如:`pipeline`)及配置選項(示例中是:`use` 選項),調用
`_context_from_use`方法, 在該方法中再次調用`get_context`方法
(序号5)輸入參數: name = 'call:cinder.api:root_app_factory',滿
足正則條件,則調用`loadcontext`方法(序号6)加載上下文, 具體的函數調
用鍊如下:
|————> loadcontext ———————————————————————|
| | (1) |(7)
| V V
|(6) _loadconfig _loadcall
| | (2) | (8)
| V V
|—— ConfigLoader.get_context FuncLoader.get_context
|(3) | ———— | |(9)
V (5)| V
ConfigLoader.find_config_section | LoaderContext.create
|(4) | |(10)
V | V
ConfigLoader._context_from_use —— | object_type.invoke
在`object_type.invoke`方法中根據協定類型調用應用的`factory`方法
(如:`cinder.api:root_app_factory`), 建立應用對象
"""
if self.absolute_name(name):
return loadcontext(object_type, name,
relative_to=os.path.dirname(self.filename),
global_conf=global_conf)
#根據配置字首及應用名稱,加載配置段
section = self.find_config_section(
object_type, name=name)
"""`defaults`配置,在建立`ConfigLoader`對象時指定, 這裡是:
{
'here':'/etc/cinder'
'__file__':'/etc/cinder/api-paste.ini'
}
"""
if global_conf is None:
global_conf = {}
else:
global_conf = global_conf.copy()
defaults = self.parser.defaults()
#用`defaults`更新`global_conf`
global_conf.update(defaults)
#根據配置端中的選項設定屬性
for option in self.parser.options(section):
#全局選項(`set`用來重寫全局選項)
if option.startswith('set '):
name = option[:].strip()
global_additions[name] = global_conf[name] = (
self.parser.get(section, option))
#全局選項(`get`使用全局變量值)
elif option.startswith('get '):
name = option[:].strip()
get_from_globals[name] = self.parser.get(section,
option)
else:
if option in defaults:
# @@: It's a global option (?), so skip it
continue
#其他的局部選項
local_conf[option] = self.parser.get(section,
option)
#用全局變量值更新局部變量
for local_var, glob_var in get_from_globals.items():
local_conf[local_var] = global_conf[glob_var]
#取得屬性中包含的過濾器(如果有的話),在`Paste Deployment`規則中,
#過濾器(filter)及應用(app)中可以包含其他的過濾器
if object_type in (APP, FILTER) and 'filter-with' in
local_conf:
filter_with = local_conf.pop('filter-with')
else:
filter_with = None
#加載指定的資源
if 'require' in local_conf:
for spec in local_conf['require'].split():
pkg_resources.require(spec)
del local_conf['require']
#根據字首建立上下文(根據配置api-paste.ini檔案中的内容就能很容易
#知道該走那個分支了,如:`composite:osapi_volume`走的就是
#`'use' in local_conf`分支)
if section.startswith('filter-app:'):
context = self._filter_app_context(
object_type, section, name=name,
global_conf=global_conf, local_conf=local_conf,
global_additions=global_additions)
elif section.startswith('pipeline:'):
context = self._pipeline_app_context(
object_type, section, name=name,
global_conf=global_conf, local_conf=local_conf,
global_additions=global_additions)
elif 'use' in local_conf:
#該方法涉及的函數調用鍊,請看上文的簡圖
context = self._context_from_use(
object_type, local_conf, global_conf,
global_additions,
section)
else:
#過濾器,走這裡。下文再具體分析
context = self._context_from_explicit(
object_type, local_conf, global_conf,
global_additions,
section)
#過濾器(filter)及應用(app)中包含其他的過濾器
if filter_with is not None:
filter_with_context = LoaderContext(
obj=None,
object_type=FILTER_WITH,
protocol=None,
global_conf=global_conf, local_conf=local_conf,
loader=self)
filter_with_context.filter_context =
self.filter_context(
name=filter_with, global_conf=global_conf)
filter_with_context.next_context = context
return filter_with_context
return context
經過上文的分析我們知道
Python Paste Deployment
系統是如何根據
api-paste.ini
配置檔案一步一步找到
osapi_volume
應用的加載入口(
cinder.api.root_app_factory
)的,完成的函數調用鍊條如下:
`loadapp`
|
V
`loadobj`
|
V
|————> `loadcontext` ———————————————————————|
| | () |()
| V V
|() `_loadconfig` `_loadcall`
| | () | ()
| V V
|—— `ConfigLoader.get_context` `FuncLoader.get_context`
|() ^——————| |()
V | V
`ConfigLoader.find_config_section` | `LoaderContext.create`
|() ()| |()
V | V
`ConfigLoader._context_from_use`—— `_APP.invoke`
|()
V
`cinder.api:root_app_factory`
下面繼續來看·
osapi_volume
應用的加載過程
加載應用
經過上文
Python Paste
的解析,我們找到了
osapi_volume
應用的處理函數,如下:
#/cinder/api/__init__.py/root_app_factory`
def root_app_factory(loader, global_conf, **local_conf):
"""輸入參數:
loader ConfigLoader執行個體
global_conf 全局配置字典 {'here':'/etc/cinder',
'__file__':'/etc/cinder/api-paste.ini'}
**local_conf 局部配置字典 {'/v2': 'openstack_volume_api_v2',
'/v1': 'openstack_volume_api_v1',
'/': 'apiversions'}
發現了吧, local_conf字典就是`api-paste.ini`中
`[composite:osapi_volume]`中包含的選項内容
"""
#根據(`cinder.conf`)中的配置執行相關的處理
#我的示例中v1及v2都是開啟的
if CONF.enable_v1_api:
LOG.warning(_LW('The v1 api is deprecated and will be'
'removed in the Liberty release. You should set'
'enable_v1_api=false and enable_v2_api=true in your'
' cinder.conf file.'))
else:
del local_conf['/v1']
if not CONF.enable_v2_api:
del local_conf['/v2']
#再次調用`Python Paste`處理應用的加載,請看下文的具體分析
return paste.urlmap.urlmap_factory(loader, global_conf,
**local_conf)
#接上文:`paste.urlmap.urlmap_factory`
def urlmap_factory(loader, global_conf, **local_conf):
#加載`not_found_app`應用,我的示例中為None
if 'not_found_app' in local_conf:
not_found_app = local_conf.pop('not_found_app')
else:
not_found_app = global_conf.get('not_found_app')
if not_found_app:
not_found_app = loader.get_app(not_found_app,
global_conf=global_conf)
#建立URLMap對象,用于存儲<path, app>映射
urlmap = URLMap(not_found_app=not_found_app)
#逐一加載`local_conf`中的應用
for path, app_name in local_conf.items():
path = parse_path_expression(path)
"""調用`ConfigLoader.get_app`加載應用,, 下文以:
`'/v2': 'openstack_volume_api_v2'`為例,分析應用的加載過程
請看下文的具體分析
"""
app = loader.get_app(app_name, global_conf=global_conf)
urlmap[path] = app
#傳回URLMap對象給調用者
return urlmap
#接上文:`loader.get_app`
#`../site-packages/paste/deploy/loadwsgi.py/_Loader/get_app`
#還記得上文`_loadconfig`中所說的吧:`ConfigLoader`對外提供三個接口
#(`get_app`, `get_server`, `get_filter`)分别用于加載不同的`WSGI`程
#序,這裡加載應用使用的就是`get_app`方法,請看:
def get_app(self, name=None, global_conf=None):
#先擷取上下文,然後建立(激活)
return self.app_context(
name=name, global_conf=global_conf).create()
def app_context(self, name=None, global_conf=None):
"""擷取name=`openstack_volume_api_v2`應用的上下文
看到ConfigLoader.get_context方法調用,是否有點印象!!!
上文擷取`osapi_volume`上下文就是通過該方法完成了的,再對比下`api-
paste.ini`檔案中兩個應用的配置,很相似吧!下文就不在重複分析了,直接
給出函數流程圖表:
`ConfigLoader.get_app`
|(1)
V
`ConfigLoader.app_context`
|(2)
V (6)
`ConfigLoader.get_context` ---------- `loadcontext`
|(3) ^——————| |(7)
V | V
`ConfigLoader.find_config_section` | `_loadcall`
|(4) (5)| |(8)
V | V
`ConfigLoader._context_from_use`—————— `FuncLoader.get_context`
|(9)
V
`LoaderContext.create`
|(10)
V
`_APP.invoke`
|(11)
V
`cinder.api.middleware.auth:pipeline_factory`
對比上文`osapi_volume`應用的函數流程圖表,可以發現處理過程基本是
一樣的,入口不一樣罷了!!!
"""
return self.get_context(
APP, name=name, global_conf=global_conf)
通過上文的分析,我們得到了
openstack_volume_api_v2
應用的加載入口
cinder.api.middleware.auth:pipeline_factory
,下面一起來看看該方法的處理過程:
#`/cinder/api/middleware/auth.py/pipeline_factory`
def pipeline_factory(loader, global_conf, **local_conf):
"""A paste pipeline replica that keys off of
auth_strategy.
輸入參數:
loader ConfigLoader執行個體
global_conf = {'__file__': '/etc/cinder/api-paste.ini',
'here': '/etc/cinder'}
local_conf = {
'keystone': 'request_id faultwrap sizelimit osprofiler'
'authtoken keystonecontext apiv2',
'noauth': 'request_id faultwrap sizelimit osprofiler'
'noauth apiv2',
'keystone_nolimit': 'request_id faultwrap sizelimit'
'osprofiler authtoken keystonecontext apiv2'}
"""
#基于配置選擇選項,我的例子中是:`keystone`
pipeline = local_conf[CONF.auth_strategy]
if not CONF.api_rate_limit:
limit_name = CONF.auth_strategy + '_nolimit'
pipeline = local_conf.get(limit_name, pipeline)
#連結清單化:['request_id', 'faultwrap', 'sizelimit',
#'osprofiler', 'authtoken', 'keystonecontext', 'apiv2']
pipeline = pipeline.split()
"""逐個加載過濾器
加載過濾器的過程和上文加載應用的邏輯類似!唯一不同的
是:object_type = _Filter, 直接給出函數調用流程圖表:
ConfigLoader.get_filter
|
V
ConfigLoader.filter_context
|
V
ConfigLoader.get_context
|
V
ConfigLoader._context_from_explicit
|
V
LoaderContext.create
|
V
_Filter.invoke
_Filter.invoke會調用各個過濾器的工廠方法(`factory`)生成對應的過
濾對象(各個過濾器的工廠方法請檢視`api-paste.ini`檔案中對應的
section)
"""
filters = [loader.get_filter(n) for n in pipeline[:-]]
"""加載應用, 由于與前述的加載過濾器的邏輯相似,這裡直接給函數調用鍊:
ConfigLoader.get_app
|
V
ConfigLoader.app_context
|
V
ConfigLoader.get_context
|
V
ConfigLoader._context_from_explicit
|
V
LoaderContext.create
|
V
_APP.create
`_APP.create`會建立`cinder.api.v2.router.APIRouter`對象,在構造
對象過程中會加載`cinder.api.contrib`下定義的擴充并建立路徑映射,相
關内容下一篇博文節再具體分析,敬請期待!!!
"""
app = loader.get_app(pipeline[-])
#反轉過濾器
filters.reverse()
"""逐一建立各個過濾器,并以`前一個過濾器`作為參數,是以最終得到的:
app = RequestId(FaultWrapper(RequestBodySizeLimiter(WsgiMiddleware(AuthProtocol(CinderKeystoneContext(APIRouter()))))))
"""
for filter in filters:
app = filter(app)
return app
經過上面的分析,
WSGI
應用就加載完成了。最終傳回給調用者的是
paste.urlmap.URLMap
對象,裡面包含三個
<path, app>
程式,這樣
cinder-api
就能根據
path
,調用指定的
app
了。
請求路由映射(Python Routes)
在上文中提到,在加載
apiv2
應用時會初始化
APIRouter
對象,該對象的頂層依賴關系如下:
`cinder.wsgi.common:Router`
^(繼承)
|
`cinder.api.openstack.__init__:APIRouter`
^(繼承)
|
`cinder.api.v2.router:APIRouter`---> (依賴)
`cinder.api.extensions:ExtensionManager`
下面來看
APIRouter
的初始化過程:
def __init__(self, ext_mgr=None):
if ext_mgr is None:
#ExtensionManager是個對象變量(類似C語言中的類變量),在對象實
#例化前指派: `cinder.api.extensions:ExtensionManager`
if self.ExtensionManager:
#執行個體化擴充管理器,内部會加載`cinder.api.contrib.*`下定義
#的擴充(子產品),子產品加載完後,以<alias, ext>字典儲存在擴充
#管理的的`extentions`字典中,請看下文的具體分析
ext_mgr = self.ExtensionManager()
else:
raise Exception(_("Must specify an "
"ExtensionManager class"))
#建立路由映射,後文分析
........
加載擴充
#`cinder.api.extensions:ExtensionManager`
def __init__(self):
# 基于`cinder.conf`檔案:CONF.osapi_volume_extension =
#cinder.api.contrib.standard_extensions
self.cls_list = CONF.osapi_volume_extension
self.extensions = {}
#加載擴充(子產品)
self._load_extensions()
#接上文:
def _load_extensions(self):
#extensions = [cinder.api.contrib.standard_extensions]
extensions = list(self.cls_list)
#循環加載擴充,基于我的配置extensions中其實隻有一個對象
for ext_factory in extensions:
"""這裡省略try{}except異常代碼塊
加載擴充`cinder.api.contrib.standard_extensions`
"""
self.load_extension(ext_factory)
#接上文:
def load_extension(self, ext_factory):
# Load the factory
#導入`cinder.api.contrib.standard_extensions`
factory = importutils.import_class(ext_factory)
#執行
factory(self)
#接上文:`cinder.api.contrib.standard_extensions`
def standard_extensions(ext_mgr):
"""參數如下:
ext_mgr cinder.api.extensions.ExtensionManager對象
LOG 全局日志對象
__path__ opt/stack/cinder/api/contrib, 包路徑
__package__ cinder.api.contrib
"""
extensions.load_standard_extensions(ext_mgr, LOG, __path__,
__package__)
#接上文:`cinder.api.extensions.py/load_standard_extensions`
def load_standard_extensions(ext_mgr, logger, path, package,
ext_list=None):
"""Registers all standard API extensions.
參數如上所示
"""
#our_dir = `opt/stack/cinder/api/contrib`
our_dir = path[]
# Walk through all the modules in our directory...
#逐個加載目錄下的子產品
for dirpath, dirnames, filenames in os.walk(our_dir):
# Compute the relative package name from the dirpath
#計算包的相對路徑:'.'
relpath = os.path.relpath(dirpath, our_dir)
if relpath == '.':
relpkg = ''
else:
relpkg = '.%s' % '.'.join(relpath.split(os.sep))
# Now, consider each file in turn, only considering
#.py files
#周遊`.py`檔案
for fname in filenames:
#将檔案名按<filename, ext>拆分
root, ext = os.path.splitext(fname)
#跳過`__init__.py`檔案
if ext != '.py' or root == '__init__':
continue
#由檔案名(如:availability_zones)得到類名
#(Availability_zones)
classname = "%s%s" % (root[].upper(), root[:])
"""得到類路徑(如):
`cinder.api.contrib.availability_zones
.Availability_zones`
"""
classpath = ("%s%s.%s.%s" %
(package, relpkg, root, classname))
if ext_list is not None and classname not in
ext_list:
logger.debug("Skipping extension: %s" %
classpath)
continue
"""省略try{}except異常處理,先來看看函數調用流程:
`ExtensionManager._load_extensions`
|
V
|-> `ExtensionManager.load_extension`
| |加載`path`子產品
| V
| `standard_extensions` or `xxx`
| |
| V
------ `load_standard_extensions`
可以看到,是通過ExtensionManager來加載子產品的,如:
`cinder.api.contrib.availability_zones
.Availability_zones`, 建立`Availability_zones`執行個體,
并注冊到`ExtensionManager`中;其他的子產品也是按照相同的模式
加載注冊的,這裡就不多說了,最後會得到一個:
`self.extensions[alias]` = ext 字典
"""
ext_mgr.load_extension(classpath)
#加載包,由于我們的例子中沒有子目錄,代碼就不再給出了
#其實原理也很簡單:直接導入包,然後執行個體化就好了
subdirs = []
for dname in dirnames:
......
建立映射路由
上文完成了擴充子產品的加載,下面繼續來看
路由的映射
過程:
#`cinder/api/openstack/__init__.py/APIRouer.__init__`
def __init__(self, ext_mgr=None):
#擴充子產品部分,請看上文
.......
#建立ProjectMapper對象,為後文的路徑映射做準備
#類繼承關系:`ProjectManager`->APIMapper->routes.Mapper
mapper = ProjectMapper()
self.resources = {}
#映射路由,請看下文的分析
self._setup_routes(mapper, ext_mgr)
#映射擴充路由,請看下文的分析
self._setup_ext_routes(mapper, ext_mgr)
#擴充`資源擴充`,請看下文的分析
self._setup_extensions(ext_mgr)
#建立`RoutesMiddleware`對象,并設定回調方法及`mapper`
super(APIRouter, self).__init__(mapper)
#接上文:映射路由
def _setup_routes(self, mapper, ext_mgr):
"""建立擷取版本資訊的路由
1.建立一個`WSGI`應用(Resource->Applications),所使用的
`controller` = `cinder.api.versions:VolumeVersion`,
2.通過`Python Routes``建立一條名為`versions`的路由,在這裡不深究
`Python Routes`的代碼實作,知道具有下述的<path, action>映射就行:
`GET` `/versions/show` `VolumeVersion.show`
"""
self.resources['versions'] = versions.create_resource()
mapper.connect("versions", "/",
controller=self.resources['versions'],
action='show')
#路徑重定向(沒有指定根路徑的映射都定向到`/`)
mapper.redirect("", "/")
"""1.建立一個`WSGI`應用(Resource->Applications),所使用的
`controller` = `cinder.api.v2.volumes:VolumeController`
2.建立路由,路徑映射為(等):
`GET` `/{project_id}/volumes/detail` `VolumeController.detail`
`POST` `/{project_id}/volumes/create`
`VolumeController.create`
`POST` `/{project_id}/volumes/:{id}/action`
`VolumeController.action`
`PUT` `/{project_id}/volumes/:{id}/action`
`VolumeController.action`
"""
self.resources['volumes'] =
volumes.create_resource(ext_mgr)
mapper.resource("volume", "volumes",
controller=self.resources['volumes'],
collection={'detail': 'GET'},
member={'action': 'POST'})
#後文的路徑映射大同小異,就不再列出了,讀者可以自行查閱
......
#接上文:映射擴充路由
def _setup_ext_routes(self, mapper, ext_mgr):
#還記得上文中加載了`cinder.api.contrib`下面的擴充子產品吧
#這裡傳回的是那些包含資源擴充(`ResourceExtentson`)的擴充子產品
#下文以`cinder.api.contrib.hosts:Hosts`擴充子產品為例,
for resource in ext_mgr.get_resources():
"""把資源擴充添加到`resources`字典
resource = `cinder.api.extensions.ResourceExtension`
resource.collection = 'os-hosts'
resource.controller =
`cinder.api.contrib.hosts.HostController`
"""
wsgi_resource = wsgi.Resource(resource.controller)
self.resources[resource.collection] = wsgi_resource
"""字典資訊如下:
{'member': {'startup': 'GET', 'reboot': 'GET',
'shutdown': 'GET'},
'controller': <cinder.api.openstack.wsgi.Resource
object at 0x5a09550>,
'collection': {'update': 'PUT'}
}
"""
kargs = dict(
controller=wsgi_resource,
collection=resource.collection_actions,
member=resource.member_actions)
if resource.parent:
kargs['parent_resource'] = resource.parent
#為資源擴充建立路由
mapper.resource(resource.collection,
resource.collection, **kargs)
if resource.custom_routes_fn:
resource.custom_routes_fn(mapper, wsgi_resource)
#接上文:擴充`資源擴充`的方法
def _setup_extensions(self, ext_mgr):
#還記得上文中加載了`cinder.api.contrib`下面的擴充子產品吧
#這裡傳回的是那些包含控制器擴充(`ControllerExtension`)的擴充子產品
#下文以`cinder.api.contrib.scheduler_hints:Scheduler_hints`
#為例
for extension in ext_mgr.get_controller_extensions():
#collection = `volumes`
#controller = `cinder.api.contrib.scheduler_hints
.SchedulerHintsController`
collection = extension.collection
controller = extension.controller
#排除不包含資源擴充的控制器擴充
if collection not in self.resources:
LOG.warning(_LW('Extension %(ext_name)s: Cannot'
' extend resource %(collection)s: No such'
' resource'),{'ext_name': extension.extension.name,
'collection': collection})
continue
#将控制器擴充注冊到資源擴充中
resource = self.resources[collection]
#注冊`wsgi actions`(wsgi_actions字典),如果包含
#`wsgi_actions`屬性的話,`SchedulerHintsController`不包含該
#屬性,是以為None
resource.register_actions(controller)
#注冊`wsgi extensions`(wsgi_extensions字典),如果包含
#`wsgi_extensions`屬性的話,`SchedulerHintsController`包含
#{create, None}的屬性,是以會建立如下映射:
#wsgi_extensions['create']
# = `SchedulerHintsController.create`
resource.register_extensions(controller)
至此
cinder-api
啟動過程中,加載
WSGI
應用及建立路由的過程就分析完成了。下一篇博文将分析
cinder-api
啟動過程中
WSGI
伺服器的啟動過程以及它是如何處理用戶端請求的。敬請期待!!!