NOVA V3 API Extension Framework分析

同時發佈於: http://leiqzhang.com/2014/02/2014-02-09-nova-v3-api-extension-framework


NOVA V3 API Extension Framework

背景

  1. 基於stable/havana分支
  2. 基於CentOS 6.4,以Redhat的RDO庫進行的環境安裝

內容

  1. V2擴展機制存在的問題
  2. Nova API V3中Plugin的實現機制和現狀
  3. 總結

V2的擴展機制存在的問題

參考V3 API framework的Spec,當前V2 API存在的問題如下:

The development of the Nova v3 API will give us the opportunity to rework the extension framework. The current framework suffers from:

  • extension specific code required to exist in core code for specific extensions to work
    • eg nova/api/openstack/compute/servers.py:Controller:create where there are hard coded references to parameters specific to extensions to pass kwargs to the server create call
  • issues around efficiency with extensions each doing their own db lookup loops
  • Can’t have CamelCase class names for extension classes due to extension loading mechanism

這裏主要展開說明前兩點。

Extension需要涉及Core Code的變動

第一個問題主要指當前的extension機制中,增加一個contrib後,往往需要在所謂的“core code”中添加必要的參數處理來將extension中引入的參數傳入到內部的API中。需要注意的是這裏的core code指的是nova.api.openstack.compute.*,即僅僅指的是API層次中核心資源相關的API Handlers,並非指的是各個Module內部的API/RPCAPI->Manager->Driver的代碼層次結構中的代碼。

以當前對核心資源“虛擬機”的create方法擴展來看,在api.openstack.compute.servers.py:create中存在大量類似如下的邏輯:

1
2
3
4
5
6
7
8
9
# optional openstack extensions:
        key_name = None
        if self.ext_mgr.is_loaded('os-keypairs'):
            key_name = server_dict.get('key_name')

        user_data = None
        if self.ext_mgr.is_loaded('os-user-data'):
            user_data = server_dict.get('user_data')
        self._validate_user_data(user_data)

如上代碼中的os-keypairs和os-user-data就是兩個extensions。當添加這倆extension後,就需要在servers.py的create方法中進行檢測和參數處理,以便最終將獲取到的key_name和user_data等參數傳遞到內部的compute_api:create方法中。

性能問題

關於第二個問題,在上文的bugs鏈接中已經描述的比較清楚,以servers爲例,問題主要是指在GET等方法中,各個extension的處理邏輯中往往需要往返回的server信息中追加extension相關的信息,如disk_config擴展需要追加存儲在DB中的disk config相關的信息等,故在各個extension中均會分別進行數據庫查詢操作,從而導致性能問題。

不過從目前HAVANA的代碼來看,此問題已經通過一種方法緩解了,但從commit來看,和V3 API並不屬於同一系列的提交。以servers爲例,此問題現在的解決辦法是,在core API處理時,就將可能需要從db中讀取出來的servers相關的信息全部一次性讀取出來並放入Cache中的Instance對象,在各個extension處理時只需要從Cache中取出所需的屬性即可。在core API中一次性讀出的信息除了instances表中一些extension添加的字段外,還會讀出關聯的instance_metadata、instance_system_metadata等多個表的信息。具體可參考nova.db.sqlalchemy.api:instance_get等相關的方法實現。

Nova V3 API Extension Framework的實現機制和現狀

實現機制

API V3中的Plugin的實現機制利用的是和Ceilometer一樣的Stevedore,此模塊主要是對python setuptools中的Entry Points機制進行了封裝。

Setuptools Entry Points

Python setuptools的Entry Points機制主要用途有兩個:Automatic Script Creation和Dynamic Discovery of Services and Plugins。

Automatic Script Creation主要功能是封裝了平臺的差異,在不同平臺上生成Python Package的入口腳本。如RDO庫中的/usr/bin目錄下的nova-api、nova-scheduler等入口腳本均是通過此機制生成的。

Dynamic Discovery of Services and Plugins主要是指本Package通過Entry Point聲明自身的“擴展點”,然後第三方Package或者本Package的各個Module可以聲明對此“擴展點”的實現。在本Package中,可以通過某種方式獲取相應“擴展點”當前所有的實現,並進行相應的處理。

Entry Point的聲明和註冊均是在各個Package的setup.cfg配置文件或者setup.py:setup method中完成的。以當前的HAVANA爲例,在nova的setup.cfg中可以看到有如下配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[entry_points]
#...

console_scripts =
    	nova-all = nova.cmd.all:main
    	nova-api = nova.cmd.api:main
	#....
nova.api.v3.extensions =
    	admin_actions = nova.api.openstack.compute.plugins.v3.admin_actions:AdminActions
    	admin_password = nova.api.openstack.compute.plugins.v3.admin_password:AdminPassword
    	agents = nova.api.openstack.compute.plugins.v3.agents:Agents
	#...
nova.api.v3.extensions.server.create =
    	availability_zone = nova.api.openstack.compute.plugins.v3.availability_zone:AvailabilityZone
    	block_device_mapping = nova.api.openstack.compute.plugins.v3.block_device_mapping:BlockDeviceMapping
	#...
#...

其中的”console_scripts”是一個特殊的entry_point,觸發的就是上述的Automatic Script Creation,在執行setup.py install時,就會根據這裏的配置在/usr/bin目錄下生成相應內容的nova-all和nova-api等文件。

後面的nova.api.v3.extensions和nova.api.v3.extensions.server.create就是此Package支持的“擴展點”,其Value中對應的list就是此擴展點對應的實現。

Python stevedore

Python setuptools的pkg_resources包中,有多種utils方法,可以獲取指定entry_point對應的所有的實現。Stevedore就是對這些方法進行了封裝,並對外提供了幾種擴展機制,具體可參考其官方網站。這裏只簡單說明下幾種:

  • DriverManager: 對應的是Driver形式的擴展機制,即使一個EntryPoint有多個實現,系統只能使用其中一個
  • HookManager:對應的是Hook機制,會遍歷EntryPoint的所有實現,並進行invoke
  • EnabledExtensionManager:在load所有實現時,增加check_func的filter機制

在ExtensionManager中,比較重要的一個方法是map(func, args, kwds),此方法會遍歷所有的extensions,並依次調用func方法。

NOVA中的現狀

NOVA中APIv3相關的代碼在nova.api.openstack.compute.pulgins.v3層次下。下面對V3 API Extension Framework的工作機制做簡單的分析:

v3 API入口

在etc.nova.api-paste.ini中,通過配置定義了/v3的URL入口,並最終指向的app_factory是nova.api.openstack.compute:APIRouterV3.factory。

加載所有Extensions

在APIRouteV3中,會通過初始化self.api_extension_manager爲stevedore.enabled.EnabledExtensionManager來加載所有聲明爲nova.api.v3.extensions的Extensions,然後會對各extensions進行相應的map調用來進行類似V2 API中的資源和Action的擴展,並會檢測配置的Core API是否全部成功load:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
    API_EXTENSION_NAMESPACE = 'nova.api.v3.extensions'
    
    # 初始化api_extension_manager並load所有的API_EXTENSION_NAMESPACE對應的extensions。
    # 如前文所述,這裏load就是nova項目中setup.cfg中配置的此namespace對應的所有extensions。當然如果有第三方的應用也聲明瞭此namespace的extension,這裏也會一併load進來。
    # 其中的check_func爲check_load_extension,其所作的工作主要包括:
    # a) 檢測是否爲相應的V3APIExtensionBase類型
    # b) extension的黑白名單過濾和驗證
    self.api_extension_manager = stevedore.enabled.EnabledExtensionManager(
            namespace=self.API_EXTENSION_NAMESPACE,
            check_func=_check_load_extension,
            invoke_on_load=True,
            invoke_kwds={"extension_info": self.loaded_extension_info})
            
    #…

    # NOTE(cyeoh) Core API support is rewritten as extensions
    # but conceptually still have core
    if list(self.api_extension_manager):
        # 對所有的extension依次進行_register_resources和_register_controllers的方法調用
        self.api_extension_manager.map(self._register_resources,
                                           mapper=mapper)
            self.api_extension_manager.map(self._register_controllers)

        #檢測配置的Core Extension是否全部成功加載
        missing_core_extensions = self.get_missing_core_extensions(
            self.loaded_extension_info.get_extensions().keys())
        if not self.init_only and missing_core_extensions:
            LOG.critical(_("Missing core API extensions: %s"),
                         missing_core_extensions)
            raise exception.CoreAPIMissing(
                missing_apis=missing_core_extensions)

其中上面代碼中的_register_resources和_register_controllers會分別調用extension的get_resources和get_controller_extensions方法,其所作的工作分別對應於V2 API中在contrib中所作的新增資源和擴展現有資源以添加action。這裏就不詳細展開。

這裏需要注意的是,V2 API中會區分Core API和Extension API。在V3 API中,所有的API均作爲extension的形式提供。如V2 API中的servers是Core API,在V3 API中也是一個普通的extension,此extension會在get_resources方法中返回servers資源對象,而get_controller_extensions方法會返回空list:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
    #nova.api.openstack.compute.pulgins.v3.servers:Servers
    
    class Servers(extensions.V3APIExtensionBase):
    """Servers."""

    name = "Servers"
    alias = "servers"
    namespace = "http://docs.openstack.org/compute/core/servers/v3"
    version = 1

    def get_resources(self):
        member_actions = {'action': 'POST'}
        collection_actions = {'detail': 'GET'}
        resources = [
            extensions.ResourceExtension(
                'servers',
                ServersController(extension_info=self.extension_info),
                member_name='server', collection_actions=collection_actions,
                member_actions=member_actions)]

        return resources

    def get_controller_extensions(self):
        return []

而當disk_config等需要對servers資源擴展action時,其所需做的就是在get_controller_extensions中返回對servers資源的擴展:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
    #nova.api.openstack.compute.pulgins.v3.disk_config
class DiskConfig(extensions.V3APIExtensionBase):
    """Disk Management Extension."""

    name = "DiskConfig"
    alias = ALIAS
    namespace = XMLNS_DCF
    version = 1

    def get_controller_extensions(self):
        servers_extension = extensions.ControllerExtension(
            self, 'servers', ServerDiskConfigController())

        return [servers_extension]
        
    #...

但是V3 API中可以通過配置約定了其中部分的API是作爲“Core Extension”的,對於Core Extension,如本節最開始的代碼所示,會在初始化時檢測是否均被正常加載。

避免各Extension之間的代碼耦合

上述兩個步驟,只是換了中方式完成了V2 API中的Extension功能,下面看V3 API中如何解決了前文所述的Extension需要涉及Core API代碼改動的問題。

V3中,主要是利用了Stevedore避免了各Extension之間的代碼耦合,從而避免了上述問題。

還是以當前對核心資源“虛擬機”的create方法擴展來看,在servers資源對應的Controller nova.api.openstack.compute.pulgins.v3.servers:ServersController中,將create作爲了一個extension的namespace,並會在初始化時就load所有的extensions:

1
2
3
4
5
6
7
8
9
10
11
    EXTENSION_CREATE_NAMESPACE = 'nova.api.v3.extensions.server.create'

    #…
    # Look for implmentation of extension point of server creation
    self.create_extension_manager = \
      stevedore.enabled.EnabledExtensionManager(
          namespace=self.EXTENSION_CREATE_NAMESPACE,
          check_func=_check_load_extension('server_create'),
          invoke_on_load=True,
          invoke_kwds={"extension_info": self.extension_info},
          propagate_map_exceptions=True)

然後在create方法中,會通過map方法依次調用各個Extensions的server_create方法:

1
2
3
4
5
6
7
8
9
10
11
12
    #create method:
    if list(self.create_extension_manager):
        self.create_extension_manager.map(self._create_extension_point,
                                              server_dict, create_kwargs)
                                              

    #….
    def _create_extension_point(self, ext, server_dict, create_kwargs):
        handler = ext.obj
        LOG.debug(_("Running _create_extension_point for %s"), ext.obj)

        handler.server_create(server_dict, create_kwargs)

在nova的setup.cfg中也配置了此namespace對應的所有的extensions,比如nova.api.openstack.compute.plugins.v3.disk_config:DiskConfig,其server_create方法如下所示,會更新ServersController:create方法中傳入的create_kwargs字典

1
2
3
    def server_create(self, server_dict, create_kwargs):
        create_kwargs['auto_disk_config'] = server_dict.get(
            'auto_disk_config')

最終,ServersController:create方法會以如下方式調用內部API來創建VM,即直接使用被各個extensions更新過的create_kwargs,而不用感知其實際的內容。

1
2
3
4
5
6
7
8
9
10
11
            (instances, resv_id) = self.compute_api.create(context,
                            inst_type,
                            image_uuid,
                            display_name=name,
                            display_description=name,
                            metadata=server_dict.get('metadata', {}),
                            access_ip_v4=access_ip_v4,
                            access_ip_v6=access_ip_v6,
                            admin_password=password,
                            requested_networks=requested_networks,
                            **create_kwargs)

總結

由上面的分析可知,如上所有的所謂的Extension,最終均依賴的還是一個內部的API,如果此API本身不具有擴展性,那麼如上所有的Extension,均只能在此內部API支持的功能的基礎上進行發揮。 比如文中提到的創建VM的內部create API,此API的參數是固定的:

1
2
3
4
5
6
7
8
9
10
    def create(self, context, instance_type,
               image_href, kernel_id=None, ramdisk_id=None,
               min_count=None, max_count=None,
               display_name=None, display_description=None,
               key_name=None, key_data=None, security_group=None,
               availability_zone=None, user_data=None, metadata=None,
               injected_files=None, admin_password=None,
               block_device_mapping=None, access_ip_v4=None,
               access_ip_v6=None, requested_networks=None, config_drive=None,
               auto_disk_config=None, scheduler_hints=None, legacy_bdm=True):

故上述各種Extension無論如何進行擴展,均不能做到使傳入到內部底層的參數變多,V3 API只是使得調用此內部API時,不需要由單個所謂的Core Extension來做邏輯處理和參數封裝了,而是通過Entry Point的機制巧妙的由各個Extension自行封裝參數而已。

而且不論是V2還是V3 Extension機制,均是在Public API層做的文章,如果要實現一個從Public API層到底層libvirt/hypervisor的新功能,不可避免是需要新增內部API的。故這裏的Extension和傳統我們理解的Plugin根本不是一回事。

參考資料


發佈了35 篇原創文章 · 獲贊 3 · 訪問量 13萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章