同時發佈於: http://leiqzhang.com/2014/02/2014-02-09-nova-v3-api-extension-framework
NOVA V3 API Extension Framework
背景
- 基於stable/havana分支
- 基於CentOS 6.4,以Redhat的RDO庫進行的環境安裝
內容
- V2擴展機制存在的問題
- Nova API V3中Plugin的實現機制和現狀
- 總結
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根本不是一回事。