1 pecan框架介紹
1.1 什么是pecan
pecan是一個輕量級的python web框架,最主要的特點是提供了簡單的配置即可創建一個wsgi對象並提供了基於對象的路由方式。
主要提供的功能點:
(1)基於對象的路由分發
(2)支持restful接口方式
(3)可拓展的安全框架
(4)可拓展的模板語言支持
(5)可拓展的json支持
(6)簡單的python配置
1.2 安裝部署
為了不影響原有環境我們使用virtualenv工具創建一個隔離的python環境來做實驗
$ virtualenv pecan-env $ cd pecan-env $ source bin/activate
安裝pecan:
$ pip install pecan
我的實驗環境是pecan 1.3.3的,可以用pecan --version命令查看
創建一個pecan項目:
$ pecan create pecan_test_project
用tree命令查看下生成的項目文件結構:

可以根據自己的需要篩減些,比如我只想要用pecan來幫我實現restful接口,那么public、templates等目錄是可以去除掉的
這里介紹幾個常用的文件或目錄:
pecan_test_project/controllers:這個目錄是用來存放要路由的對象類和要調用的對象方法的
pecan_test_project/model:用來存放模型的,比如一個數據庫表的model,做ORM映射時會用到
pecan_test_project/tests:可以用來寫一些單元測試
pecan_test_project/app.py:該文件用來控制如何構建你的pecan應用,該文件里會包含setup_app函數用來生成和返回一個wsgi app,一般來說,該文件是不用再修改的
config.py:該文件定義了服務的ip和端口號、服務的根目錄類等和日志設置等。
直接執行pecan serve config.py命令服務就開始跑起來了
默認監聽IP是0.0.0.0,端口號是8080
然后直接在頁面訪問,比如我的服務器是192.168.0.107,則瀏覽器訪問http://192.168.0.107:8080就會看到如下頁面了

這是因為我們訪問的是它的根路徑,根據pecan的基於對象的分發,它對調用如下的index方法,返回一個index.html頁面了(這個index方法是被默認為當get請求分發到這個對象時會被路由到該方法,它其實是包裹在了expose里,可以理解它為get方法):

1.3 安裝部署pecan的對象分發路由策略
Pecan的根路徑是通過配置文件中指定的某個類開始的,比如上面的RootController類,並且會把請求路徑按/分成多份,比如/test1/test2/test3,則會從RootController找test1對象,再從test1對象中找test2對象,再從test2對象中找test3對象,最后調用test3類對象的方法。
舉個例子:
修改root.py文件為:
from pecan import expose, redirect from webob.exc import status_map class BooksController(object): @expose() def index(self): return "Welcome to book section." @expose() def bestsellers(self): return "We have 5 books in the top 10." class CatalogController(object): @expose() def index(self): return "Welcome to the catalog." books = BooksController() class RootController(object): @expose(generic=True, template='index.html') def index(self): return dict() @index.when(method='POST') def index_post(self, q): redirect('https://pecan.readthedocs.io/en/latest/search.html?q=%s' % q) @expose('error.html') def error(self, status): try: status = int(status) except ValueError: # pragma: no cover status = 500 message = getattr(status_map.get(status), 'explanation', '') return dict(status=status, message=message) catalog = CatalogController()
則如下幾個url就會對象如下幾個方法:
http://192.168.0.107:8080/catalog --> CatalogController類index方法
http://192.168.0.107:8080/catalog/books/bestsellers --> BooksController類bestsellers方法
這里主要是通過expose裝飾器將類對象的方法暴露出來,使得能夠被路由到。Expose中還能指定要返回的對象格式,比如json的,則expose(‘json’)
基於http的請求方法來選擇方法:
class BaseMethodController(object): # HTTP GET / @expose(generic=True, template='json') def index(self): return dict() # HTTP POST / @index.when(method='POST', template='json') def index_POST(self, **kw): return kw
在RootController類下加上:
basemethod = BaseMethodController()
可以用curl命令來測試:
curl http://127.0.0.1:8080/basemethod/ curl http://127.0.0.1:8080/basemethod/ -X POST -d 'hello=world'
pecan也提供了一些額外的特殊方法來提高url處理的靈活性,有_lookup()、_default()和_route()等方法。
_lookup():
它可以獲取一個或多個參數並經過處理后返回一個新的控制器對象並把未處理的url保存於remainder中繼續由新的控制器來路由。
class Student(object): def __init__(self, name): self.name = name class StudentController(object): def __init__(self, student): self.student = student @expose() def name(self): return self.student.name class LookupTest(object): @expose() def _lookup(self, name, *remainder): print 'enr' student = Student(name) if student: return StudentController(student), remainder else: abort(404)
在RootController類下加上:
look_test = LookupTest()
測試:
curl http://127.0.0.1:8080/look_test/66/name
_default():
當其它方法沒匹配上時就調用_default方法
_route():
該方法允許你完全覆蓋pecan原有的路由機制,pecan也使用該方法實現了RestController。
1.4 pecan的請求和返回對象
我們知道每一個http都是有一個請求對象和相應對象的,這在pecan中是分別對應着pecan.request和pecan.response對象
比如我可以賦值pecan.response.status = 403
1.5 RestController
pecan已經幫我們封裝好了RestController的接口,有如下:

舉一個簡單的get例子:
class MyRestController(rest.RestController): @expose() def get(self, id): return id
在RootController類下加上:
my_restcontroller = MyRestController()
測試:
curl http://127.0.0.1:8080/my_restcontroller/13579
1.6 一些常用配置項
hook機制配置
hook機制可以使得我們控制當一個http請求在被pecan框架執行過程中各個關鍵點上執行相對應的代碼,以下是四個關鍵點:
on_route:當url還沒被路由前調用
before:路由到某個函數但執行代碼之前調用
after:執行了代碼之后調用
on_error:執行代碼過程中發生錯誤調用
舉例:
在app.py的同級目錄下創建my_hooks.py文件:
from pecan.hooks import PecanHook class SimpleHook(PecanHook): def on_route(self, state): print 'it is on route' def before(self, state): print 'it is before exec' def after(self, state): print 'it is after exec' def on_error(self, state, exc): print 'it is on error'
修改app.py文件,加上hoos:

比如一個比較常用的是在這里獲取數據庫連接對象,在路由到的方法里就可以通過request獲取到數據庫連接對象進行操作了,省去了重復獲取代碼。比如:

日志配置:
修改config.py文件:

默認是console的,可以改成logfile形式
在controllers如下編寫即可:

1.7 Pecan和WSGI
部署pecan的wsgi app一般有很多種方式,但一般來說,可以使用deploy()函數生成任何Pecan應用程序的WSGI入口點。

這里舉我自己項目中部署的兩種方式:
(1)使用Werkzeug通用庫

這里的第6行返回的就是使用pecan框架的deploy.loadapp()函數產生的wsgi app
Werkzeug就是用python對WSGI的實現一個通用庫。它是Flask所使用的底層WSGI庫
Serving是Werkzeug庫的一個用來運行wsgi的函數,將wsgi app跑起來

(2)Apache + mod_wsgi的方式
這種方式通過依托於Apache服務的形式,隨Apache啟動時一同啟動wsgi app。
這里以gnocchi-api服務為例講解
查看該服務的.wsgi配置文件:

這里重點是看紅框里的app文件,這個文件定義了怎么生成wsgi app
查看app文件:

再查看gnocchi.rest模塊下的app文件:


可以看到其實最后也是使用deploy.loadapp生成一個wsgi app,然后由appache來運行這個wsgi app。
這里我們用Apache + mod_wsgi的方式來運行我們實例的pecan程序:
(1)首先安裝httpd服務和mod_wsgi包
yum install httpd mod_wsgi -y
(2)部署pecan服務
cd pecan_test_project python setup.py build python setup.py install
(3)創建好用戶和配置文件
創建test1用戶:
adduser test1
創建配置文件:
/etc/httpd/conf.d/simple_wsgi.conf:
Listen 0.0.0.0:8080 <VirtualHost *:8080> ServerName thinstack-JxRcqm ## Vhost docroot DocumentRoot "/var/www/simpleapp/" <Directory "/var/www/simpleapp/"> Options Indexes FollowSymLinks MultiViews AllowOverride None Require all granted </Directory> ## Logging ErrorLog "/var/log/httpd/simpleapp_wsgi_error.log" ServerSignature Off CustomLog "/var/log/httpd/simpleapp_wsgi_access.log" combined SetEnvIf X-Forwarded-Proto https HTTPS=1 WSGIApplicationGroup %{GLOBAL} WSGIDaemonProcess test1 display-name=simpleapp_wsgi group=test1 processes=4 threads=4 user=test1 WSGIProcessGroup test1 WSGIScriptAlias / "/var/www/simpleapp/app.wsgi" </VirtualHost>
/var/www/simpleapp/config.py:
# Server Specific Configurations from pecan_test_project.my_hooks import SimpleHook server = { 'port': '8080', 'host': '0.0.0.0' } # Pecan Application Configurations app = { 'root': 'pecan_test_project.controllers.root.RootController', #'hooks': lambda: [SimpleHook], 'modules': ['pecan_test_project'], 'static_root': '%(confdir)s/public', 'template_path': '%(confdir)s/pecan_test_project/templates', 'debug': True, 'errors': { 404: '/error/404', '__force_dict__': True } } logging = { 'root': {'level': 'INFO', 'handlers': ['console']}, 'loggers': { 'pecan_test_project': {'level': 'DEBUG', 'handlers': ['console'], 'propagate': False}, 'pecan': {'level': 'DEBUG', 'handlers': ['console'], 'propagate': False}, 'py.warnings': {'handlers': ['console']}, '__force_dict__': True }, 'handlers': { 'console': { 'level': 'DEBUG', 'class': 'logging.StreamHandler', 'formatter': 'color' }, }, 'formatters': { 'simple': { 'format': ('%(asctime)s %(levelname)-5.5s [%(name)s]' '[%(threadName)s] %(message)s') }, 'color': { '()': 'pecan.log.ColorFormatter', 'format': ('%(asctime)s [%(padded_color_levelname)s] [%(name)s]' '[%(threadName)s] %(message)s'), '__force_dict__': True } } }
/var/www/simpleapp/app.wsgi:
from pecan.deploy import deploy application = deploy('/var/www/simpleapp/config.py')
(4)啟動服務
systemctl start httpd
查看服務狀態:

用瀏覽器訪問:http://192.168.0.107:8080/my_restcontroller/13579
就可以看到響應值了
注意點:
Python setup.py install安裝報這種錯時:

執行:
pip install soupsieve
1.8 實例項目下載地址
https://github.com/luohaixiannz/pecan_test_project
2 Paste + PasteDeploy + Routes
2.1 Paste + PasteDeploy + Routes
這里以nova項目來舉例
在一些較老的openstack的核心組件中用的都是比較傳統的Paste + PasteDeploy + Routes + WebOb的方式來實現Restful API,比如nova組件,后來openstack社區的人受不了這繁瑣的方式,就在新的組件里采用了pecan框架來構建Restful API,這里以nova組件為例進行該種方式的解析
首先我們看下如何使用Paste + PasteDeploy來構建nova的wsgi應用,查看nova關於paste的配置文件,/etc/nova/api-paste.ini:
############ # Metadata # ############ [composite:metadata] use = egg:Paste#urlmap /: meta [pipeline:meta] pipeline = cors metaapp [app:metaapp] paste.app_factory = nova.api.metadata.handler:MetadataRequestHandler.factory [composite:osapi_compute] use = call:nova.api.openstack.urlmap:urlmap_factory /: oscomputeversions /v2: openstack_compute_api_v21_legacy_v2_compatible /v2.1: openstack_compute_api_v21 [composite:openstack_compute_api_v21] use = call:nova.api.auth:pipeline_factory_v21 noauth2 = cors http_proxy_to_wsgi compute_req_id faultwrap sizelimit osprofiler noauth2 osapi_compute_app_v21 keystone = cors http_proxy_to_wsgi compute_req_id faultwrap sizelimit osprofiler authtoken keystonecontext osapi_compute_app_v21 [composite:openstack_compute_api_v21_legacy_v2_compatible] use = call:nova.api.auth:pipeline_factory_v21 noauth2 = cors http_proxy_to_wsgi compute_req_id faultwrap sizelimit osprofiler noauth2 legacy_v2_compatible osapi_compute_app_v21 keystone = cors http_proxy_to_wsgi compute_req_id faultwrap sizelimit osprofiler authtoken keystonecontext legacy_v2_compatible osapi_compute_app_v21 [filter:request_id] paste.filter_factory = oslo_middleware:RequestId.factory [filter:compute_req_id] paste.filter_factory = nova.api.compute_req_id:ComputeReqIdMiddleware.factory [filter:faultwrap] paste.filter_factory = nova.api.openstack:FaultWrapper.factory [filter:noauth2] paste.filter_factory = nova.api.openstack.auth:NoAuthMiddleware.factory [filter:osprofiler] paste.filter_factory = nova.profiler:WsgiMiddleware.factory [filter:sizelimit] paste.filter_factory = oslo_middleware:RequestBodySizeLimiter.factory [filter:http_proxy_to_wsgi] paste.filter_factory = oslo_middleware.http_proxy_to_wsgi:HTTPProxyToWSGI.factory [filter:legacy_v2_compatible] paste.filter_factory = nova.api.openstack:LegacyV2CompatibleWrapper.factory [app:osapi_compute_app_v21] paste.app_factory = nova.api.openstack.compute:APIRouterV21.factory [pipeline:oscomputeversions] pipeline = cors faultwrap http_proxy_to_wsgi oscomputeversionapp [app:oscomputeversionapp] paste.app_factory = nova.api.openstack.compute.versions:Versions.factory ########## # Shared # ########## [filter:cors] paste.filter_factory = oslo_middleware.cors:filter_factory oslo_config_project = nova [filter:keystonecontext] paste.filter_factory = nova.api.auth:NovaKeystoneContext.factory [filter:authtoken] paste.filter_factory = keystonemiddleware.auth_token:filter_factory
可以看到這個配置文件是ini格式的,都是由[type:name]這種形式為單元,在paste中,type包括以下幾種類型:
(1)應用:app,application
(2)過濾器:filter,filte-app
(3)管道:pipeline,一般結合添加多個過濾器的時候使用(最后一個是wsgi應用)
(4)組合:composite,表示它由若干個應用和若干個過濾器構成
下面分析個nova比較熟悉的
[composite:osapi_compute] use = call:nova.api.openstack.urlmap:urlmap_factory /: oscomputeversions /v2: openstack_compute_api_v21_legacy_v2_compatible /v2.1: openstack_compute_api_v21
這是一個composite類型,看到它的key是use表明它使用了paste中的urlmap的功能,該功能是根據url前綴請求路由到不同的WSGI應用
其中use可以使用以下幾種形式:
egg:使用一個URI指定的egg包中的對象
call:使用某個模塊中的可調用對象
config:使用另外一個配置文件
osapi_compute是的use用的是call,查看urlmap_factory的實現:
def urlmap_factory(loader, global_conf, **local_conf): 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 = URLMap(not_found_app=not_found_app) for path, app_name in local_conf.items(): path = paste.urlmap.parse_path_expression(path) app = loader.get_app(app_name, global_conf=global_conf) urlmap[path] = app return urlmap
這里的 local_conf.items()是在枚舉/、/v2和/v2.1
逐個加載生成app,並與path對應起來保存到urlmap中
這里以/v2.1作為分析
[composite:openstack_compute_api_v21] use = call:nova.api.auth:pipeline_factory_v21 noauth2 = cors http_proxy_to_wsgi compute_req_id faultwrap sizelimit osprofiler noauth2 osapi_compute_app_v21 keystone = cors http_proxy_to_wsgi compute_req_id faultwrap sizelimit osprofiler authtoken keystonecontext osapi_compute_app_v21
查看pipeline_factory_v21實現:
def pipeline_factory_v21(loader, global_conf, **local_conf): """A paste pipeline replica that keys off of auth_strategy.""" return _load_pipeline(loader, local_conf[CONF.api.auth_strategy].split()) def _load_pipeline(loader, pipeline): filters = [loader.get_filter(n) for n in pipeline[:-1]] app = loader.get_app(pipeline[-1]) filters.reverse() for filter in filters: app = filter(app) return app
可以看到pipeline_factory_v21函數中根據配置文件auth_strategy的指定來選擇對應配置,項目中默認是使用keystone,於是使用了這行的配置:
keystone = cors http_proxy_to_wsgi compute_req_id faultwrap sizelimit osprofiler authtoken keystonecontext osapi_compute_app_v21
接着調用_load_pipeline函數先生成osapi_compute_app_v21的app,接着加載各個filter來按序封裝app,最后返回一個經過filter封裝后的app應用
這里看下生成osapi_compute_app_v21的app
[app:osapi_compute_app_v21]
paste.app_factory = nova.api.openstack.compute:APIRouterV21.factory
查看factory實現:
@classmethod def factory(cls, global_config, **local_config): """Simple paste factory, :class:`nova.wsgi.Router` doesn't have one.""" return cls()
生成一個APIRouterV21類實例,查看__init__實現方法:
def __init__(self): self._loaded_extension_info = extension_info.LoadedExtensionInfo() super(APIRouterV21, self).__init__()
先通過加載擴展插件信息(一個controller可當做是一個擴展插件)保存到_loaded_extension_info變量中,再調用父類__init__方法:
def __init__(self): def _check_load_extension(ext): return self._register_extension(ext) 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}) mapper = ProjectMapper() self.resources = {} # NOTE(cyeoh) Core API support is rewritten as extensions # but conceptually still have core if list(self.api_extension_manager): # NOTE(cyeoh): Stevedore raises an exception if there are # no plugins detected. I wonder if this is a bug. self._register_resources_check_inherits(mapper) self.api_extension_manager.map(self._register_controllers) LOG.info(_LI("Loaded extensions: %s"), sorted(self.loaded_extension_info.get_extensions().keys())) super(APIRouterV21, self).__init__(mapper)
這里的關鍵點在於mapper 實例和_register_resources_check_inherits函數調用生成映射關系
在_register_resources_check_inherits函數中通過遍歷加載每一個擴展插件並且通過調用_register_resources函數來生成該擴展插件的url映射關系保存到mapper 中
_register_resources函數實現關鍵代碼:

在收集了該擴展插件的信息比如collection和member信息后通過調用mapper實例的resource方法來生成url和controller類方法的對應關系,collection和member是在擴展插件里定義的方法名稱,用來后面routes進行映射生成。
查看resource方法實現:

紅框標注的是關鍵調用,通過調用routes模塊的方法來生成映射關系。
routes模塊根據用戶傳入的信息如collection和member信息生成url映射,如果沒有傳入也會默認生成常用的映射url,比如/xxx/的get請求對應到index方法、/xx/的put請求對應到update方法等。
所以這里生成一個app的過程其實也把url映射關系建立起來了
如果想了解它生成過程細節,可查看:
/usr/lib/python2.7/site-packages/routes/mapper.py文件的Mapper類的resource方法,這里列出一些關鍵注釋和調用:

從注釋中我們可以看到這些選項都是可選的,就算不傳任何選項,它也會對controller生成一些常用的映射,collection主要用於給用戶指定一些特定的跟get請求相關的,member主要用於給用戶指定一些特定的跟post相關的,比如nova/api/openstack/compute/servers.py中的指定:

函數關鍵代碼:

通過調用connect方法來保存生成的映射,connect方法實現在:
nova/api/openstack/__init__.py該文件中:

最后又是調回到routes模塊中去保存url映射
再舉一個filter keystonecontext的生成實現:
[filter:keystonecontext]
paste.filter_factory = nova.api.auth:NovaKeystoneContext.factory
查看factory的實現(它繼承了Middleware):


可以看到filter的特性是傳入了app或filter對象給它,到最后它會在完成自己的調用后,調用下一個filter或者是app
2.2 查看url映射和查看請求解析結果
(1)列出當前所有url映射的方法
編輯文件:
/usr/lib/python2.7/site-packages/nova/api/openstack/__init__.py:

保存退出后重啟服務成功可在日志中找到如下日志:

(2)獲取具體controller類和方法名
查看某個請求通過url映射解析后得到的controller類和其對應的action方法從而找到具體的調用函數。
編輯該文件:
/usr/lib/python2.7/site-packages/nova/api/openstack/__init__.py:

重啟nova-api服務
然后比如做列出雲主機列表操作,就可以在日志中找到如下日志:

第一個紅框是請求url,第二個紅框是通過路由模塊解析后生成的結果,可以看到它映射到的controller類是:nova.api.openstack.compute.servers.ServersController
方法是detail
我們可以很容易的找到該方法(nova/api/openstack/compute/servers.py文件中):

2.3 在原有controller上簡單新增一個api
以Servers該controller為例,在上面添加一個創建快照的api
剛才我們看到nova/api/openstack/compute/servers.py文件的Servers的生成url映射的規則是這樣寫的:

其實我們只要在member_actions中加多一項就好了,比如修改為:
member_actions = {'action': 'POST', 'create_snapshot_test': 'POST'}
然后重啟服務,再通過我們2.2.1中說的列出所有映射url的方法來查看下是否生成映射了:

如果想要測試則可以在novaclient中寫一個發起/server/{server_id}/create_snapshot_test的url的http請求來測試
這里還有一點,我們看到Servers類只自定義了個action方法,但Servers類里有很多這種方法定義:

其實它們都是通過action引申的,也就是根據根據body中傳的action的值來再路由到具體的方法,看紅框中的wsgi.action裝飾器就是起到這個功能的,所以一個action可以對應到多個方法,但url路徑都是/nova/servers/action,但傳的參數的action值可以指定,我們可以通過查看novaclient里構建http請求代碼可以看出來:


可以看到url路徑還是/servers/{server_id}/action,但body數據中包含了action鍵的值為revertResize
所以我們也可以仿照實現一個api,比如加一個回滾快照的api:

在novaclient對應的發請求里可以這樣寫:

2.4 創建一個擴展插件到nova組件上
這里我以創建一個名為groups的擴展插件舉例如何在nova服務中添加擴展插件
創建控制資源文件
nova/api/openstack/compute/groups.py:
# --*-- coding:utf8 --*-- import webob from webob import exc from nova import compute from nova.api.openstack.compute.schemas import groups from nova.api.openstack import extensions from nova.api.openstack import wsgi from nova.api import validation from nova import exception from nova import db from nova.i18n import _ from nova.policies import groups as groups_policies #from thvmware.manage import VCenterManagement from nova import context from oslo_log import log as logging from nova.console import type as ctype import nova.conf from oslo_utils import uuidutils from nova.consoleauth import rpcapi as consoleauth_rpcapi import requests import traceback CONF = nova.conf.CONF LOG = logging.getLogger(__name__) ALIAS = 'groups' class GroupsController(wsgi.Controller): def __init__(self): super(GroupsController, self).__init__() self.context = context.get_admin_context() self.consoleauth_rpcapi = consoleauth_rpcapi.ConsoleAuthAPI() self.compute_api = compute.API() @extensions.expected_errors(404) @validation.schema(groups.create) def create(self, req, body): context = req.environ['nova.context'] context.can(groups_policies.BASE_POLICY_NAME) context.can(groups_policies.POLICY_ROOT % 'create') LOG.info(body) @extensions.expected_errors((400, 404)) def add_group(self, req, body): context = req.environ['nova.context'] ret = {'ret_state':'failed'} values = body['group'] if not values.has_key('group_id') or values['group_id'] == '': values['group_id'] = uuidutils.generate_uuid() try: db.add_instance_group(context, values) except Exception,e: LOG.error('%s', traceback.format_exc()) ret['error'] = e.message return ret ret['ret_state'] = 'success' return ret @extensions.expected_errors(()) def get_groups(self, req): ret = {'ret_state': 'failed'} context = req.environ['nova.context'] try: groups = db.get_groups(context) ret['data'] = groups except Exception, e: LOG.error('%s', traceback.format_exc()) ret['error'] = e.message return ret ret['ret_state'] = 'success' return ret class Groups(extensions.V21APIExtensionBase): """extended common support.""" name = "Groups" alias = ALIAS version = 1 def get_resources(self): member_actions = {'update': 'POST', 'detail': 'GET'} collection_actions = {'add_group': 'POST', 'get_groups': 'GET'} resources = [] # mapper = Route name Methods Path # mapper.connect = <bound method ProjectMapper.connect of <cinder.api.openstack.ProjectMapper object at 0x2e5bc10>> # # 示例: # # map.resource("message", "messages", collection={"rss": "GET"}) # # "GET /message/rss" => ``Messages.rss()``. # # map.resource('message', 'messages', member={'mark':'POST'}) # # "POST /message/1/mark" => ``Messages.mark(1)`` res = extensions.ResourceExtension( ALIAS, GroupsController(), collection_actions=collection_actions, member_actions=member_actions) resources.append(res) return resources def get_controller_extensions(self): return []
創建API會調用到的數據結構定義文件
nova/api/openstack/compute/schemas/groups.py:
from nova.api.validation import parameter_types create = { 'type': 'object', 'properties': { 'MyTest': { 'type': 'object', 'properties': { 'myhost': parameter_types.hostname, }, 'additionalProperties': False, }, }, 'additionalProperties': False, }
創建權限設定文件:
nova/policies/groups.py:
from oslo_policy import policy from nova.policies import base BASE_POLICY_NAME = 'os_compute_api:groups' POLICY_ROOT = 'os_compute_api:groups:%s' groups_policies = [ policy.RuleDefault( name=BASE_POLICY_NAME, check_str=base.RULE_ADMIN_API), policy.RuleDefault( name=POLICY_ROOT % 'create', check_str=base.RULE_ANY), policy.RuleDefault( name=POLICY_ROOT % 'discoverable', check_str=base.RULE_ANY), policy.RuleDefault( name=POLICY_ROOT % 'show', check_str=base.RULE_ANY), ] def list_rules(): return groups_policies
注冊API:
nova/policies/__init__.py:
from nova.policies import groups ...... def list_rules(): return itertools.chain( ...... groups.list_rules(), ......
添加entry_points:
nova.egg-info/entry_points.txt:
[nova.api.v21.extensions] ........ groups = nova.api.openstack.compute.groups:Groups ........
重啟nova-api服務:
可以在日志中看到Loaded extensions中加載的插件中包含了我們新加的插件名字:

也可以使用命令nova list-extensions查看加載的插件
通過之前查看url映射的方法也可以看到建立的映射:

