转自:探索 OpenStack 之(11):cinder-api Service 启动过程分析 以及 WSGI / Paste deploy / Router 等介绍
OpenStack 中的每一个提供 REST API Service 的组件,比如 cinder-api,nova-api 等,其实是一个 WSGI App,其主要功能是接受客户端发来的 HTTP Requst,然后进行用户身份校验和消息分发。代码实现上,主要使用了几种技术:WSGI Server、WSGI Application、Paste deploy 和 Router。本文希望结合 cinder-api 的启动过程,对这些相关技术和 cinder-api 的代码做一个总结。
0. WSGI Server、Paste deploy、Router相关知识
0.1 WSGI Server
参考文档:
PEP 333 - Python Web Server Gateway Interface v1.0
http://wiki.woodpecker.org.cn/moin/WSGI
简单来说,python中的 WSGI 是 Python 应用程序或框架与 Web 服务器之间的一种接口,它定义了一套接口来实现服务器与应用端的通信规范,它将 web 组件分为三类:
- web 服务器(Service):接受客户端发来的 request,并返回 app 产生的 response 发回给客户端
- web 应用程序(App): 每个 app 是一个 callable 对象。一个函数, 方法, 类, 或者实现了 __call__ 方法的实例对象都可以用来作为应用程序对象。服务器每次收到 http 客户端发来的请求都会调用相应的应用程序的 __call__ 方法去去处理。
- web 中间件(Middleware):某些对象(app)可以在一些应用程序面前是服务器, 而从另一些服务器看来却是应用程序。可参考 http://k0s.org/mozilla/craft/middleware.html。
(1)WSGI Server唯一的任务就是接收来自 client 的请求,然后将请求传给 application,最后将 application 的response 传递给 client。
(2)在使用 Middleware 的情况下,WSGI的处理模式为 WSGI Server -> (WSGI Middleware)* -> WSGI Application。其实 middleware 本身已是一个 app,只是你需要有一个 App 做为实现主要功能的 Application。Middleware可以:
- 可以根据目的 URL 将一个请求分发 (routing) 给不同的应用程序对象, 并对 environ 做相应修改。这种 middleware 称为 router。
- 允许多个应用程序或框架在同一个进程中一起运行。可以依次调用多个 middleware。
- 通过分析 request,在网络上转发请求和响应, 进行负载均衡和远程处理.
- 对 response 内容进行后加工, 比如应用 XSL 样式.
(3)在 Server 和 Application 之间,可以使用若干个 Middleware 来处理 request 和 response:
具体过程描述如下:
- 服务器为 Applicaiton 实例化一个 WSGIService 实例,实例化过程中调用 Paste Deployment 来加载/注册各 middleware 和app。
- 服务器启动 WSGIService , 包括创建 socket,监听端口,然后等待客户端连接。
- 当有请求来时,服务器解析客户端信息放到环境变量 environ 中,并调用绑定的 handler 来处理请求。handler 解析这个 http 请求,将请求信息例如 method,path 等放到 environ 中。wsgi handler还会将一些服务器端信息也放到 environ 中,最后服务器信息,客户端信息,本次请求信息全部都保存到了环境变量environ中
- wsgi handler 调用注册的各 app (包括 middleware) 来处理 request,比如 middleware 来进行数据检查、整理、用户验证等, wsgi app 生成 reponse header/status/body 回传给wsgi handler。
- response 生成以后和回传之前,一些等待 response 的 被阻塞的 middleware 的方法可以对response 进行进一步的处理,比如添加新的数据等
- 最终 handler 通过socket将response信息塞回给客户端
以 cinder-api 为例,其 WSGI 实例初始化和启动 service 的代码如下:
- launcher = service.process_launcher()
- server = service.WSGIService('osapi_volume') # 使用 Paste delopment 方式 加载 osapi_volume 这个 application,这过程中包括加载各 middleware app
- launcher.launch_service(server, workers=server.workers) launcher.wait() #真正启动service
0.2 Paste delpoy
Paste deploy 是一种配置 WSGI Service 和 app 的方法:
- Spec文档: Paste Deployment
- Python paste deploy 的实现代码: https://bitbucket.org/ianb/pastedeploy
- 这篇 文章 也解释了 OpenStack 使用 Paste deployment 的方法
简单来说,它提供一个方法 loadapp,它可以用来从 config 配置文件或者 Python egg 文件加载 app(包括middleware/filter和app),它只要求 app 给它提供一个入口函数,该函数通过配置文件告诉Paste depoly loader。一个简单的调用 loadapp 的 Python code 的例子:
from paste.deploy import loadapp wsgi_app = loadapp('config:/path/to/config.ini')
/path/to/config.ini 文件:
[app:myapp] paste.app_factory = myapp.modulename:factory
myapp.modulename:factory 的实现代码:
@classmethod def factory(cls, global_conf, **local_conf): """Factory method for paste.deploy.""" return cls
OpenStack 的 Paste deploy 使用的是各组件的 api-paste.ini 配置文件,比如 /etc/cinder/api-paste.ini。OpenStack WSGI app 是个实现了 __call__ 方法的类 class Router(object) 的一个实例对象。OpenStack WSGI middleware 是个实现了 __call__ 方法的类 class Middleware(Application) 类的子类的实例。
0.3 Route
Route 是用 Python 重新实现的 Rails routes system,它被用来做将 URL 映射为 App 的 action,以及为 App的action 产生 URL。
1. 两个重要的方法:map.connect (定义映射规则) 和 map.match (获取映射结果)。一个简单例子:
# Setup a mapper from routes import Mapper map = Mapper() map.connect(None, "/error/{action}/{id}", controller="error") map.connect("home", "/", controller="main", action="index") # Match a URL, returns a dict or None if no match result = map.match('/error/myapp/4') # result == {'controller': 'error', 'action': 'myapp', 'id': '4'}
2. map.resource 方法
当映射规则很多的时候,需要使用很多次 map.connect,这时可以使用 map.resource 方法。其定义为:resource(member_name, collection_name, **kwargs). 有很多种定义映射规则的方法,具体见 Routes 的官方文档。
(1) map.resource("message","messages",controller=a)
第一个参数 message 为 member_name(资源名),第二个参数 messages 为 collection_name(资源集合名),一般定义资源集合名为资源名的复数。该规则对应于:
map.connect('/messages',controller=a,action='index',conditions={'method':['GET']}) map.connect('/messages',controller=a,action='create',conditions={'method':['POST']}) map.connect('/messages/{id}',controller=a,action='show',conditions={'method':['GET']}) map.connect('/messages/{id}',controller=a,action='update',conditions={'method':['PUT']}) map.connect('/messages/{id}',controller=a,action='delete',conditions={'method':['DELETE']})
(2) map.resource('message', 'messages',controller=a, collection={'search':'GET','create_many':'POST'}, member={'update_many':'POST','delete_many':'POST'})
map.resource除了默认的路由条件外,还可以额外的定义‘资源集合的方法’以及‘单个资源的方法’
collection={'search':'GET','create_many':'POST'} 定义了资源集合方法 search,其curl动作为GET,create_many,其curl动作为POST
member={'update_many':'POST','delete_many':'POST'} 定义了单个资源方法 update_many,其curl动作为POST,delete_many,其curl动作为POST
(3) map.resource('message', 'messages',controller=a,path_prefix='/{projectid}', collection={'list_many':'GET','create_many':'POST'}, member={'update_many':'POST','delete_many':'POST'})
map.resource 初始化时还可以指定 curl 访问路径的前缀路径,没有指定时默认为collection_name(资源集合名)路径为path_prefix/collection_name。
参考文档:
http://routes.readthedocs.org/en/latest/setting_up.html
http://blog.csdn.net/bellwhl/article/details/8956088
1. 启动 Cinder api/scheduler/volume service 的总体过程
Cinder 的 cinder-api 是 WSGIService 类型,其它服务是 Service 类型。这两个类的定义在文件 cinder/cinder/service.py 中。具体步骤如下:
(1). 用户调用 cinder/bin/ 目录中的脚本来启动相关服务,比如 cinder-all 会启动cinder所有服务,cinder-api 会启动 cinder-api服务。以 cinder-api 为例,它会启动名为 osapi_volume 的 WSGI Service:
if __name__ == '__main__': CONF(sys.argv[1:], project='cinder', version=version.version_string()) logging.setup("cinder") utils.monkey_patch() rpc.init(CONF) launcher = service.process_launcher() server = service.WSGIService('osapi_volume') #初始化WSGIService,load osapi_volume app launcher.launch_service(server, workers=server.workers) launcher.wait() #启动该 WSGI service
在执行 server = service.WSGIService('osapi_volume') 时,首先会 load app。具体 load 过程下面 2 Paste 加载 osapi_volume 的过程。
class WSGIService(object):
"""Provides ability to launch API from a 'paste' configuration."""
def __init__(self, name, loader=None):
self.name = name
self.manager = self._get_manager()
#load APP
self.loader = loader or wsgi.Loader() #paste.deploy.loadwsgi.ConfigLoader
self.app = self.loader.load_app(name)
def load_app(self, name):
"""Return the paste URLMap wrapped WSGI application.
"""
try:
return deploy.loadapp("config:%s" % self.config_path, name=name) #从配置文件 paste-api.ini 中加载WSGI application
except LookupError as err:
LOG.error(err)
raise exception.PasteAppNotFound(name=name, path=self.config_path)
......
self.server = wsgi.Server(name,self.app,host=self.host,port=self.port) #定义WSGI Server
执行 launcher.launch_service(server, workers=server.workers) 会启动该 service。它会绑定网卡 osapi_volume_listen=0.0.0.0 并在 osapi_volume_listen_port=5900 端口监听,并且可以启动 osapi_volume_workers=<None> 个worker线程。
- osapi_volume_listen=0.0.0.0 # IP address on which OpenStack Volume API listens (string # value)
- osapi_volume_listen_port=8776 # Port on which OpenStack Volume API listens (integer value)
- osapi_volume_workers=<None> # Number of workers for OpenStack Volume API service. The # default is equal to the number of CPUs available. (integer # value)
(3). 启动cinder-scheduler 会启动一个名为 cinder-scheduler 的 Service。与 cinder-api 的WSGI Service 不同的是,它需要创建 RPC 连接,启动消费者线程,然后等待队列消息。它的启动参数主要包括:
- report_interval = 10 #(IntOpt) Interval, in seconds, between nodes reporting state to datastore
- periodic_interval = 60 #(IntOpt) Interval, in seconds, between running periodic tasks
- periodic_fuzzy_delay = 60 #(IntOpt) Range, in seconds, to randomly delay when startingthe periodic task scheduler to reduce stampeding. (Disable by setting to 0)
(4). 启动cinder-volume的过程类似于 cinder-scheduler。
2. 使用 Paste deploy 加载 osapi_volume 的过程
2.1 总体过程
osapi-volume 服务启动的过程中,调用 deploy.loadpp 使用 config 方式从 paste-api.conf 文件来 load 名为osapi_volume 的应用,其入口在文件的 [composite:osapi_volume]部分:
[composite:osapi_volume] #osapi_volume 即 deploy.loadapp方法中传入的name
use = call:cinder.api:root_app_factory /: apiversions /v1: openstack_volume_api_v1 /v2: openstack_volume_api_v2
调用 cinder/api/__init__.py 的 root_app_factory 方法:
def root_app_factory(loader, global_conf, **local_conf): #local_conf 包括 /,/v1,/v2 三个pair
if CONF.enable_v1_api:
LOG.warn(_('The v1 api is deprecated and will be removed after the Juno release. You should set enable_v1_api=false and '
'enable_v2_api=true in your cinder.conf file.'))
else:
del local_conf['/v1']
if not CONF.enable_v2_api:
del local_conf['/v2'] return paste.urlmap.urlmap_factory(loader, global_conf, **local_conf) def urlmap_factory(loader, global_conf, **local_conf): 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
在该方法中,如果 cinder.conf 中 enable_v1_api = False 则不加载 V1对应的app;如果 enable_v2_api = False 则不加载V2对应的app。否则三个都会被加载,所以在不需要使用 Cinder V1 API 的情况下,建议 disable V1 来节省 cinder-api 的启动时间和资源消耗。
加载 openstack_volume_api_v2,它对应的composite 是:
[composite:openstack_volume_api_v2] use = call:cinder.api.middleware.auth:pipeline_factory noauth = request_id faultwrap sizelimit osprofiler noauth apiv2 keystone = request_id faultwrap sizelimit osprofiler authtoken keystonecontext apiv2 keystone_nolimit = request_id faultwrap sizelimit osprofiler authtoken keystonecontext apiv2
cinder/api/middleware/auth.py 中的 pipeline_factory 方法如下:
def pipeline_factory(loader, global_conf, **local_conf): """A paste pipeline replica that keys off of auth_strategy.""" pipeline = local_conf[CONF.auth_strategy] #读取CONF.auth_strategy,其值默认为 token,再获取 'token' 对应的 pipeline pipeline = pipeline.split() # ['request_id', 'faultwrap', 'sizelimit', 'osprofiler', 'authtoken', 'keystonecontext', 'apiv2'] filters = [loader.get_filter(n) for n in pipeline[:-1]] #依次使用 deploy loader 来 load 各个 filter app = loader.get_app(pipeline[-1]) #使用 deploy loader 来 load app 即 apiv2 filters.reverse() #[<function _factory at 0x7fe44485ccf8>, <function auth_filter at 0x7fe44485cc80>, <function filter_ at 0x7fe43fe5f398>, <function _factory at 0x7fe43fe59ed8>, <function _factory at 0x7fe43fe595f0>, <class 'cinder.openstack.common.middleware.request_id.RequestIdMiddleware'>] for filter in filters: app = filter(app) #然后依次调用每个 filter 来 wrapper 该 app return app ,
它首先会读取 cinder.conf 中 auth_strategy 的值。我们一般会使用 token,所以它会从 local_conf 中读取keystone 的 pipeline 。
第一步,loader.get_filter(filter)来使用 deploy loader 来 load 每一个 filter,它会调用每个 filter 对应的入口 factory 函数,该函数在不同的 MiddleWare 基类中定义,都是返回函数 _factory 的指针(keystonemiddleware 是 函数auth_filter的指针)。比如:
Enter Middleware.factory with cls: <class 'cinder.openstack.common.middleware.request_id.RequestIdMiddleware'> Enter cinder.wsgi.factory with cls: <class 'cinder.api.middleware.fault.FaultWrapper'> factory /usr/lib/python2.7/dist-packages/cinder/wsgi.py:385 Enter cinder.wsgi.factory with cls: <class 'cinder.api.middleware.sizelimit.RequestBodySizeLimiter'> factory /usr/lib/python2.7/dist-packages/cinder/wsgi.py:385 Enter cinder.wsgi.factory with cls: <class 'cinder.api.middleware.auth.CinderKeystoneContext'> factory /usr/lib/python2.7/dist-packages/cinder/wsgi.py:385
第二步,app = loader.get_app(pipeline[-1]):使用 deploy loader 来 load app apiv2,调用入口函数 cinder.api.v2.router:APIRouter.factory,factory 函数会调用其__init__方法,这方法里面会setup routes,setup ext_routes 和 setup extensions 等,具体分析见 2.3 章节。
第三步,app = filter(app):然后调用每个 filter 来 wrapper APIRouter。
依次调用 filter 类的 factory 方法或者 keystonemiddleware 的 audit_filter方法,传入 app 做为参数。
以keystonemiddleware为例,在 audit_filter 方法中,首先会初始化个 AuthProtocol 的实例,再调用其__init__ 函数,会设置 auth token 需要的一些环境,比如_signing_dirname、_signing_cert_file_name 等。
到这里,osapi_volume 的 loading 就结束了。在此过程中,各个 filter middleware 以及 WSGI Application 都被加载/注册,并被初始化。WSGI Server 在被启动后,开始等待 HTTP request,然后进入HTTP request 处理过程。
2.2 Cinder 中的资源、Controller 操作及管理
在进入具体的 load apiv2 application 过程之前,我们先来了解了解下 Cinder 里面的资源管理。
2.2.1 Cinder 的资源(Resource)类型
OpenStack 定义了两种类型的资源:
- Core resource (Resource): 核心资源。核心资源的定义文件在 /cinder/api/v2/ 目录下面。Cinder 的核心资源包括:volumes,types,snapshots,limits等。
- Extension resource: 扩展资源也是资源,使用同核心资源,只是它们的地位稍低。在 ./cinder/api/contrib/ 目录下面有很多文件,这些文件定义的都是扩展资源,例如 quotas.py 等。扩展资源又分为两种情况:
- 一种扩展资源本身也是一种资源,只是没那么核心,比如 os-quota-sets。对扩展资源的访问方法同核心资源,比如 PUT /v2/2f07ad0f1beb4b629e42e1113196c04b/os-quota-sets/2f07ad0f1beb4b629e42e1113196c04b
- 另一种扩展资源是对核心资源的扩展(Resource extension),包括对 action 的扩展和基本操作的扩展,现在的 Cinder 中只有对 Resource 基本操作的扩展,例如 SchedulerHints 是对 volumes 提供了扩展方法。一些扩展资源同时具备这两种功能。
Extension resource 的加载见下面 2.3.1 load extension 章节。
Cinder 中的Extension resource 包括 extensions,os-hosts,os-quota-sets,encryption,backups,cgsnapshots,consistencygroups,encryption,os-availability-zone,extra_specs,os-volume-manage,qos-specs,os-quota-class-sets,os-volume-transfer,os-services,scheduler-stats。
2.2.2 Cinder 的对资源(Resource)的操作
(1)基本操作
- 基本操作: 即核心资源和扩展资源拥有的最基本的操作,使用 HTTP Method 比如 GET, PUT, POST,DELETE 等方法访问这些资源。
- 以 volumes 为例,其 Controller 类 VolumeController 定义了对 volumes 的 index,create,delete,show, update等。比如 GET /v2/{tenant-id}/volumes/detail。
- 对于这类操作,http://developer.openstack.org/api-ref-blockstorage-v2.html 有详细的列表。
(2)action
- Action: 资源扩展拥有的使用 @wsgi.action(alias) 装饰的方法,这些方法是Core Resource 的 CRUD 基本操作不能满足的对资源的操作。它们本身是看做 Core resource 的访问方法,只是访问方法和 CRUD 的访问方法不同。
- 比如: volumes 的扩展资源的 Controller 类 VolumeAdminController 定义的 os-migrate_volume_completion action:
@wsgi.action('os-migrate_volume_completion') #该方法的 alias 是 os-migrate_volume_completion
def _migrate_volume_completion(self, req, id, body)
- 使用:使用 HTTP Post method,在 URL 中使用其 Core resource 的 alias 比如 ’volumes‘,'action' method,在Rquest body 中包含 action 的 alias 和 参数。比如:
POST /{tenant-id}/volumes/{volume-id}/action
body: {"os-force_delete": null}
(3)extends
- extends: 资源扩展使用 @wsgi.extends 装饰了的函数。extends 是对某个 Core Resource 的某个 CURD 方法比如 create, index,show,detail 等的扩展,你在使用标准 HTTP Method 访问 Core resource 时,可以附加 extension 信息,在 response 中你可以得到这些方法的output。尚不清楚实际应用场景。
- 例如: volumes 的资源扩展 SchedulerHints 的 Controller 类 SchedulerHintsController 定义了 volumes.create 方法的 extends 方法:
@wsgi.extends
def create(self, req, body) - 以OS-SCH-HNT/SchedulerHints 扩展的 volumes 的 create 方法为例,其使用方法如下: POST http://9.123.245.88:8776/v2/2f07ad0f1beb4b629e42e1113196c04b/volumes
body:
{
"volume": {
"availability_zone": null,
"source_volid": null,
"description": null,
"snapshot_id": null,
"size": 1,
"name": "hintvolume3",
"imageRef": null,
"volume_type": null,
"metadata": {}
},
"OS-SCH-HNT:scheduler_hints": {"near": "2b7c42eb-7736-4a0f-afab-f23969a35ada"}
- Cinder 在接收到核心资源的 CRUD 访问 Request 时,在调用其基本功能的前面或者后面(根据extend 方法的具体实现决定是前面还是后面),它会找到该核心资源的所有扩展了该方法的所有资源扩展,再分别调用这些资源扩展中的方法,这些方法的 output 会被插入到 volumes 的 create 方法产生的 response 内。
(4)操作的操作对象:操作对象分两类:collection 和 member。 collection 比如对 volumes 的操作,POST 到 /volumes 会创建新的volume; member 比如对单个volume 的操作:GET /volumes/{volume-ID} 返回指定 volume 的信息。
(5)操作的输出的(反)序列化 - (de)serializers
@wsgi.response(202)
@wsgi.serializers(xml=BackupTemplate)
@wsgi.deserializers(xml=CreateDeserializer)
def create(self, req, body): #BackupsController 的 create 方法
- 对于有输入的操作,在调用其方法前需要把 HTTP Request body 反序列化再传给该方法。如果不使用默认的 deserializer,你可以指定你实现的特定 deserializer。以 os-force_delete 为例,它没定义特定的 deserializer,所以 Cinder 会根据其 Request content-type 选择默认的 deserializer。
def deserialize(self, meth, content_type, body): #比如 body: {"os-force_delete": null} meth_deserializers = getattr(meth, 'wsgi_deserializers', {}) #获取该方法的 deserializer try: mtype = _MEDIA_TYPE_MAP.get(content_type, content_type) if mtype in meth_deserializers: deserializer = meth_deserializers[mtype] else: deserializer = self.default_deserializers[mtype] #没有定义则使用默认的 deserializer except (KeyError, TypeError): raise exception.InvalidContentType(content_type=content_type) return deserializer().deserialize(body) #对 request body 反序列化,比如 {'body': {u'os-force_delete': None}}
- 对于有输出的操作,在调用该操作后,需要序列化其输出。除了跟 content-type 配对的默认 serializer 外,你还可以自定义序列化方法。代码:
-
def _process_stack(self, request, action, action_args,content_type, body, accept):
...... # Run post-processing extensions if resp_obj: _set_request_id_header(request, resp_obj) # Do a preserialize to set up the response object serializers = getattr(meth, 'wsgi_serializers', {}) #获取该方法的 serializer resp_obj._bind_method_serializers(serializers) if hasattr(meth, 'wsgi_code'): resp_obj._default_code = meth.wsgi_code resp_obj.preserialize(accept, self.default_serializers) # Process post-processing extensions response = self.post_process_extensions(post, resp_obj,request, action_args) if resp_obj and not response: response = resp_obj.serialize(request, accept, self.default_serializers) #序列化方法的 output 为 response
(6)以下表格显示了 Cinde 中部分 Resource extensions 的 actions 和 extends:
Collection/Resource | Extension name | File (/api/contrib 目录下) | Controller | wsgi actions | wsgi extends |
volumes | SchedulerHints | scheduler_hints.py |
SchedulerHintsController
|
|
('create', None)
|
VolumeTenantAttribute | volume_tenant_attribute.py |
VolumeTenantAttributeController
|
|
('detail', None), ('show', None)
| |
AdminActions |
VolumeAdminController
|
{'os-migrate_volume_completion': '_migrate_volume_completion', 'os-reset_status': '_reset_status', 'os-force_delete': '_force_delete', 'os-migrate_volume': '_migrate_volume', 'os-force_detach': '_force_detach'}
| |||
VolumeReplication | volume_replication.py | VolumeReplicationController | {'os-reenable-replica': 'reenable', 'os-promote-replica': 'promote'} | ('show', None), ('detail', None) | |
VolumeUnmanage | volume_unmange.py | VolumeUnmanageController | {'os-unmanage': 'unmanage'} | ||
VolumeActions | volume_actions.py | VolumeActionsController | {'os-reserve': '_reserve', 'os-roll_detaching': '_roll_detaching', 'os-terminate_connection': '_terminate_connection', 'os-set_bootable': '_set_bootable', 'os-unreserve': '_unreserve', 'os-detach': '_detach', 'os-retype': '_retype', 'os-volume_upload_image': '_volume_upload_image', 'os-update_readonly_flag': '_volume_readonly_update', 'os-begin_detaching': '_begin_detaching', 'os-extend': '_extend', 'os-initialize_connection': '_initialize_connection', 'os-attach': '_attach'} | ||
VolumeHostAttribute VolumeMigStatusAttribute VolumeImageMetadata | ('detail', None), ('show', None) | ||||
types | TypesManage | types_manage.py |
VolumeTypesManageController
|
{'create': '_create', 'delete': '_delete'}
| |
VolumeTypeEncryption |
| ||||
limits | UsedLimits | used_limits.py |
UsedLimitsController
| ||
backups | AdminActions |
BackupAdminController
|
{'os-reset_status': '_reset_status', 'os-force_delete': '_force_delete'}
| ||
snapshots | SnapshotActions | snapshot_actions.py |
SnapshotActionsController
|
{'os-update_snapshot_status': '_update_snapshot_status'} | |
AdminActions |
SnapshotAdminController
|
{'os-reset_status': '_reset_status', 'os-force_delete': '_force_delete'}
| |||
ExtendedSnapshotAttributes |
其中,Admin actions 是必须 Administrator 才能操作,以volumes 的 os-force-delete 为例:
URL:http://9.123.245.88:8776/v2/2f07ad0f1beb4b629e42e1113196c04b/volumes/bac9c184-e375-4191-9602-93a1c74c5336/action
Method:POST
body:{"os-force_delete": null}
2.2.3 管理 Resource 几个类
(1)APIRouter
APIRouter 是 Cinder 中的核心类之一,它负责分发 HTTP Request 到其管理的某个 Resource:
- 它的几个重要属性
- 属性 resources 是个数组,用来保存所有的 Resource 的 Controller 类的instance;每个Resource 拥有一个该数组的数组项,比如 self.resources['versions'] = versions.create_resource() 会为 versions 核心资源创建一个 Resource 并保存到 resources 中。
- 属性 mapper 用来保存保存所有 resource, extension resource,resource extension 的 routes 供 RoutesMiddleware 使用。它其实是一张路由表。 每个表项表示一个 URL 和 controller 以及 action 的映射关系,每个 controller 就是 Resource 的一个实例。比如:
{'action': u'detail', 'controller': <cinder.api.openstack.wsgi.Resource object at 0x7fa137be8950>, 'project_id': u'fa2046aaead44a698de8268f94759fc1'}
- 属性 routes 是 routes.middleware.RoutesMiddleware 的实例,它其实是一个 WSGI app,它使用 mapper 和 _dispatch_进行初始化。功能是根据 URL 得到 controller 和它的 action。
在 cinder-api service 启动阶段,它的 __init__ 方法被调用,会 setup resource routes,setup extension resource routes 以及 resource extension 的routes,然后把生成的 mapper 传给初始化了的 routes.middleware.RoutesMiddleware。
ext_mgr = self.ExtensionManager() #初始化 ext_mgr,用于管理所有的 extensions mapper = ProjectMapper() #生成 mapper self.resources = {} #初始化 resources 数组 self._setup_routes(mapper, ext_mgr) #为核心资源建立 routes self._setup_ext_routes(mapper, ext_mgr) #为扩展资源建立 routes self._setup_extensions(ext_mgr) #保存资源扩展的方法 super(APIRouter, self).__init__(mapper) #初始化RoutesMiddleware
在处理 HTTP Request 阶段,它负责接收经过 Middleware filters 处理过的 HTTP Request,再把它转发给 RoutesMiddleware 实例,它会使用 mapper 得到 Request 的URL 对应的 controller 和 action,并设置到 Request 的 environ 中,然后再调用 APIRoutes 的 dispatch 方法。该方法会从 environ 中得到controller,其实是个 Resource 的实力,然后调用其 __call_ 方法,实现消息的分发。
(2) Controller
了解上面的各种资源之后,我们还需要了解 Controller,所谓的 Controller 实际上就是代表对该资源的操作集合,每个 Resource 都有一个相应的 Controller 类,其基类在 /cinder/api/v2/wsgi.py 中定义,在每个资源对应的 py 文件中定义对应的资源的Controller 类。
以 Volumes 为例,其 Controller 子类 VolumeController 在 /api/v2/volumes.py 中定义 class VolumeController(wsgi.Controller)。它包含上面描述的对 volumes 的基本操作操作。
(3)ExtensionManager
既然存在这么多的资源,Cinder 需要使用一个集中的方式对他们进行管理。OpenStack 使用 ExensionManager 对这些扩展资源进行统一的管理。此类提供如下方法:
- def __init__(self)
- def register(self, ext) #注册一个extension
- def get_resources(self) #获取Extension resource
- def get_controller_extensions(self) #获取Resource extension
- def _check_extension(self, extension) #检查指定的 extension
- def load_extension(self, ext_factory) #加载所有的extensions
cinder.conf 的 osapi_volume_extension 配置项 (其默认值是 'cinder.api.contrib.standard_extensions')告诉 ExtensionManager 去哪里找扩展的类文件,默认的路径是 /cinder/api/contrib 目录下的 py 文件。
(4) ProjectMapper
APIRouters 使用一个 mapper 属性来保存为所有resource 建立的 mapping,该 mapper 是个ProjectManager 的实例。其集成关系是 ProjectMapper -> APIMapper -> routes.Mapper,它提供一个重要的方法 def match(self, url=None, environ=None) 来根据其 routes 来获取映射关系。
以上这些类之间的关系如下:
从上图可以看出 APIRouter 类的支配地位:
(1)它被deploy loadapp调用, 然后它调用 ExtensionManager 的方法去获取各个Resource;保存 mapper,router,resource 等所有数据
(2)它接受Middleware filters 处理过的 Request,交给 router (RoutesMiddleware) 去做 URL 匹配,然后交给匹配得到的 Resource 去做消息分发。
2.3 Cinder REST API Rotues 建立过程
该过程为 Cinder 各 Resource 建立 Routes。
2.3.1 load extension
APIRourter 调用类 ExtensionManager 的 __init__ 方法, 它再调用其 _load_extensions 方法获取所有的 extensions,其过程如下:
1. 首先读取 cinder.conf 中有配置项 osapi_volume_extension, 其默认值为 cinder.api.contrib.standard_extensions。该配置项可以设置多个value。
2. 使用 importutils.import_class('cinder.api.contrib.standard_extensions'),然后调用方法其入口方法 load_standard_extensions,这方法又会调用 extensions.load_standard_extensions(ext_mgr, LOG, __path__, __package__)
3. 该方法会对 /cinder/api/contrib 目录下所有的 py 文件中的类调用 load_extension 来加载。
ExtensionManager 类会遍历 contrib 目录下每个 py 文件,检查其中的 class 定义,判断它是哪一种资源。比如处理 volume_type_encryption.py 文件:
class Volume_type_encryption(extensions.ExtensionDescriptor): """Encryption support for volume types.""" def get_resources(self): #ExtensionManager 遍历 contrib 文件夹下每个文件的时候会尝试调用这个方法。如果这个方法存在,则该类会被认为是个Extension Resource。 resources = [] res = extensions.ResourceExtension( Volume_type_encryption.alias, #Extension resource alias VolumeTypeEncryptionController(), parent=dict(member_name='type', collection_name='types')) resources.append(res) return resources def get_controller_extensions(self): #ExtensionManager遍历每个文件的时候会尝试调用本方法。如果该方法存在,则认为该类提供了Resource extensions controller = VolumeTypeEncryptionController() extension = extensions.ControllerExtension(self, 'types', controller) #这里的‘types'就是被扩展了的 Core Resource 名字 return [extension]
4. load 每个extension时,会执行每个 extension 类的 __init__ 方法,并将其实例指针存放在 ExtensionManager 的 extensions 内,供以后调用。
Ext name: VolumeMigStatusAttribute
Ext alias: os-vol-mig-status-attr
至此,该 loading extensions 完成。下图以 Loaded extension os-vol-mig-status-attr 为例描述其具体过程:
2.3.2 load routes
APIRouter 的方法 _setup_routes 方法 为每个Core Resource 类建立 URL 到其 Controller 类的 method 的映射规则。
以 volumes Resource 为例,其 mapper 规则为:
1 self.resources['volumes'] = volumes.create_resource(ext_mgr) 2 mapper.resource("volume", "volumes", controller=self.resources['volumes'], collection={'detail': 'GET'}, member={'action': 'POST'})
第1行:会该 Resource 创建一个 Resource 实例,初始化其controller 为VolumeController,调用 Resource 的 __init__ 方法去获取 Controller 的 wsgi_actions 并保存(其实都是空的),Resource 实例会被保存到 APIRouter 的 resources 数组中以’volume‘ 为键值的一个数组项。其实 Resource 是个 wsgi Application,将来会调用它的 __call__ 方法去做消息分发。
第2行:定义了一个将 URL 映射到 VolumeController method 的规则:
- 如果 URL 是 /volumes/{volume ID} 并且 HTTP method 是 POST,那么该 URL 会被映射到 action 方法,该 action 会被转化为 VolumeController的具体方法。
- 如果 URL 是 /volumes 并且HTTP Method 是 GET,那么映射到 detail 方法,其对应的是 VolumeConroller 的 detail 方法。
注意这里 Core resource 的 mapper 的处理单个Resource 的 action (member={'action': 'POST'})和处理Resource集合的 action (collection={'detail': 'GET'})都是hard coded 的。 所有的映射规则都会被保存到 APIRouter 类的 mapper 属性中。
2.3.3 load extension routes
建立所有 Extension resource 的 routes。
方法 _setup_ext_routes 在类 cinder.api.v2.router.APIRouter 中实现:
- 首先调用 ExtensionManager.get_resources 获取所有的 extension resource (它同样会在 /api/contrib 目录下所有文件中进行遍历,如果一个类实现了 get_controller_extensions 方法,则加载其 extension resource)
- 为每个 extension resource 对应的 Controller 类的每个 Member 和 Collection 方法建立映射规则 (mapper.resource(resource.collection, resource.collection, **kargs)),该规则同样保存在 APIRouter 的 mapper 属性中。以 os-snapshot-actions 为例,其 mapper 为:
mapper.resource('snapshot', 'snapshots', {'member': {}, 'controller': <cinder.api.openstack.wsgi.Resource object at 0x7f854ab630d0>, 'collection': {'detail': 'GET'}})
2.3.4 Setup (Controller) extensions
APIRouter 类的 _setup_extensions 方法遍历每个extension,找到每个 extension 的 wsgi actions 和 wsgi extends,保存到其 resources 数组的该 extension 对应的 resource 数组项中。
通过以上几个步骤,APIRouter 为所有的 Resouce 和 Extension resource 建立了URL 到 Resource 的 Controller 类的方法的映射规则并保存到 mapper 属性中,还保存了Resource extension 的方法到 Resource 中。
我们来看看 APIRouter 到底是怎么保存和使用这些信息的:
(0)以 os-extend 为例,URL 中使用 'action', Request body 中带有具体的action: {"os-extend": {"new_size": 2}}
(1)APIRoutes 有个数组属性 resources,每一个数组项是每一个资源拥有的 class Resource(wsgi.Application) 实例。该实例的 wsgi_actions 属性保存该Resource 支持的所有 actions,每个 action 的数据是个 pair (alias,action 对应的 method 的Controller 类实例的地址)。以 volumes 为例,
{'os-migrate_volume_completion': <bound method VolumeAdminController._migrate_volume_completion of <cinder.api.contrib.admin_actions.VolumeAdminController object at 0x7f459d256b90>>, 'os-reserve': <bound method VolumeActionsController._reserve of <cinder.api.contrib.volume_actions.VolumeActionsController object at 0x7f459d254d10>>, 'os-promote-replica': <bound method VolumeReplicationController.promote of <cinder.api.contrib.volume_replication.VolumeReplicationController object at 0x7f459d237d10>>, ...... 'os-attach': <bound method VolumeActionsController._attach of <cinder.api.contrib.volume_actions.VolumeActionsController object at 0x7f459d254d10>>}
(2)Resource 还有个 wsgi_extensions 数组属性,它为每一个 Resource 的基本方法保存扩展的方法。以 volumes 的 detail 方法为例,它有两个扩展方法:
[<bound method VolumeTenantAttributeController.detail of <cinder.api.contrib.volume_tenant_attribute.VolumeTenantAttributeController object at 0x7f459d3a5c90>>, <bound method VolumeReplicationController.detail of <cinder.api.contrib.volume_replication.VolumeReplicationController object at 0x7f459d237d10>>]
(3)收到一个 action 后,RoutesMiddleware 根据 URL 找到 Resource 的地址,然后 Resource 从 request body 中取出 action 的 alias,然后查找该 pairs,得到真正的method。
(4)首先会查找wsgi_extensions,获取该 method 对应的所有扩展方法的Controller 类,分别调用其method; 然后再把 request dispatch 到该方法上得到其输出了。
wsgi_actions['os-extend']: <bound method VolumeActionsController._extend of <cinder.api.contrib.volume_actions.VolumeActionsController object at 0x7f459d254d10>>
(5)该 Resource 实例同样还使用属性 wsgi_action_extensions 保存其所有的 action extensions。
2.3.5 调用 RoutesMiddleware 的 __init__ 方法
将RouterMiddleware 后面要执行的 WSGI app 即 def _dispatch(req), 和上面步骤中生成的 mapper 传给 RoutesMiddleware,完成它的初始化,其实 RoutesMiddleware 也是一个 WSGI middleware app,只是它没有定义在 api-paste.ini 文件中,而是在 APIRouter 里面显式调用。将来它会根据传入的 mapper 进行 URL 和 Controller action 的匹配。
至此,cinder-api 中的各 Middleware (Filters) 和 Application (APIRoute,RoutesMiddleware)都完成了注册和初始化,cinder-api WSGI Service 进程也启动完毕,可以接受REST API Request 了。
3. Cinder 目录下文件的用途
Cinder 目录下的文件都按照不同的用途分别放在不同的文件夹中,小结如下:
下一篇文章将分析 cinder-api 处理 HTTP Request 的过程。