OpenStack Restful API框架介绍
2021-05-19 11:28
标签:rac pat core 监听 重复 redirect hello 结果 apache 1 pecan框架介绍 1.1 什么是pecan pecan是一个轻量级的python web框架,最主要的特点是提供了简单的配置即可创建一个wsgi对象并提供了基于对象的路由方式。 主要提供的功能点: (1)基于对象的路由分发 (2)支持restful接口方式 (3)可拓展的安全框架 (4)可拓展的模板语言支持 (5)可拓展的json支持 (6)简单的python配置 1.2 安装部署 为了不影响原有环境我们使用virtualenv工具创建一个隔离的python环境来做实验 安装pecan: 我的实验环境是pecan 1.3.3的,可以用pecan --version命令查看 创建一个pecan项目: 用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文件为: 则如下几个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的请求方法来选择方法: 在RootController类下加上: 可以用curl命令来测试: pecan也提供了一些额外的特殊方法来提高url处理的灵活性,有_lookup()、_default()和_route()等方法。 _lookup(): 它可以获取一个或多个参数并经过处理后返回一个新的控制器对象并把未处理的url保存于remainder中继续由新的控制器来路由。 在RootController类下加上: 测试: _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例子: 在RootController类下加上: 测试: 1.6 一些常用配置项 hook机制配置 hook机制可以使得我们控制当一个http请求在被pecan框架执行过程中各个关键点上执行相对应的代码,以下是四个关键点: on_route:当url还没被路由前调用 before:路由到某个函数但执行代码之前调用 after:执行了代码之后调用 on_error:执行代码过程中发生错误调用 举例: 在app.py的同级目录下创建my_hooks.py文件: 修改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包 (2)部署pecan服务 (3)创建好用户和配置文件 创建test1用户: 创建配置文件: /etc/httpd/conf.d/simple_wsgi.conf: /var/www/simpleapp/config.py: /var/www/simpleapp/app.wsgi: (4)启动服务 查看服务状态: 用浏览器访问:http://192.168.0.107:8080/my_restcontroller/13579 就可以看到响应值了 注意点: Python setup.py install安装报这种错时: 执行: 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: 可以看到这个配置文件是ini格式的,都是由[type:name]这种形式为单元,在paste中,type包括以下几种类型: (1)应用:app,application (2)过滤器:filter,filte-app (3)管道:pipeline,一般结合添加多个过滤器的时候使用(最后一个是wsgi应用) (4)组合:composite,表示它由若干个应用和若干个过滤器构成 下面分析个nova比较熟悉的 这是一个composite类型,看到它的key是use表明它使用了paste中的urlmap的功能,该功能是根据url前缀请求路由到不同的WSGI应用 其中use可以使用以下几种形式: egg:使用一个URI指定的egg包中的对象 call:使用某个模块中的可调用对象 config:使用另外一个配置文件 osapi_compute是的use用的是call,查看urlmap_factory的实现: 这里的 local_conf.items()是在枚举/、/v2和/v2.1 逐个加载生成app,并与path对应起来保存到urlmap中 这里以/v2.1作为分析 查看pipeline_factory_v21实现: 可以看到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 查看factory实现: 生成一个APIRouterV21类实例,查看__init__实现方法: 先通过加载扩展插件信息(一个controller可当做是一个扩展插件)保存到_loaded_extension_info变量中,再调用父类__init__方法: 这里的关键点在于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映射关系建立起来了 再举一个filter keystonecontext的生成实现: 查看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 创建一个扩展插件到nova组件上 这里我以创建一个名为groups的扩展插件举例如何在nova服务中添加扩展插件 创建控制资源文件 nova/api/openstack/compute/groups.py: 创建API会调用到的数据结构定义文件 nova/api/openstack/compute/schemas/groups.py: 创建权限设定文件: nova/policies/groups.py: 注册API: nova/policies/__init__.py: 添加entry_points: nova.egg-info/entry_points.txt: 重启nova-api服务: 可以在日志中看到Loaded extensions中加载的插件中包含了我们新加的插件名字:
也可以使用命令nova list-extensions查看加载的插件 通过之前查看url映射的方法也可以看到建立的映射:
OpenStack Restful API框架介绍 标签:rac pat core 监听 重复 redirect hello 结果 apache 原文地址:https://www.cnblogs.com/luohaixian/p/11706616.html$ virtualenv pecan-env
$ cd pecan-env
$ source bin/activate
$ pip install pecan
$ pecan create pecan_test_project
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()
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
basemethod = BaseMethodController()
curl http://127.0.0.1:8080/basemethod/
curl http://127.0.0.1:8080/basemethod/ -X POST -d ‘hello=world‘
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)
look_test = LookupTest()
curl http://127.0.0.1:8080/look_test/66/name
class MyRestController(rest.RestController):
@expose()
def get(self, id):
return id
my_restcontroller = MyRestController()
curl http://127.0.0.1:8080/my_restcontroller/13579
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‘
yum install httpd mod_wsgi -y
cd pecan_test_project
python setup.py build
python setup.py install
adduser test1
Listen 0.0.0.0:8080
# 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
}
}
}
from pecan.deploy import deploy
application = deploy(‘/var/www/simpleapp/config.py‘)
systemctl start httpd
pip install soupsieve
############
# 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
[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
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
[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
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
[app:osapi_compute_app_v21]
paste.app_factory = nova.api.openstack.compute:APIRouterV21.factory
@classmethod
def factory(cls, global_config, **local_config):
"""Simple paste factory, :class:`nova.wsgi.Router` doesn‘t have one."""
return cls()
def __init__(self):
self._loaded_extension_info = extension_info.LoadedExtensionInfo()
super(APIRouterV21, self).__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)
[filter:keystonecontext]
paste.filter_factory = nova.api.auth:NovaKeystoneContext.factory
# --*-- 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 =
from nova.api.validation import parameter_types
create = {
‘type‘: ‘object‘,
‘properties‘: {
‘MyTest‘: {
‘type‘: ‘object‘,
‘properties‘: {
‘myhost‘: parameter_types.hostname,
},
‘additionalProperties‘: False,
},
},
‘additionalProperties‘: False,
}
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
from nova.policies import groups
......
def list_rules():
return itertools.chain(
......
groups.list_rules(),
......
[nova.api.v21.extensions]
........
groups = nova.api.openstack.compute.groups:Groups
........