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

 

posted @ 2019-10-20 04:24  luohaixian  阅读(3087)  评论(0编辑  收藏  举报