天天看點

Openstack liberty 中Cinder-api啟動過程源碼分析1

在前面的博文中,主要分析了

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

指令(如:

/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

服務對象

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

應用

上文的

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

伺服器的啟動過程以及它是如何處理用戶端請求的。敬請期待!!!

繼續閱讀