OpenStack Restful API框架介紹


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映射的方法也可以看到建立的映射:

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM