由于openstack项目是通过RESTful API向外提供服务,比如当我们通过openstackclient创建虚拟机时,openstackclient不直接调用创建虚拟机的具体python lib,而是发送一个http请求,当nova组件接收到请求后再调用具体函数和其它服务去创建虚拟机。这种机制使openstack具有很好的扩展性和可移植性,但也为我们阅读源码带来了困难,特别对于初学者想寻找函数入口添加远程断点都比较困难(openstack已经有上百万行代码了)。本文主要介绍如何去确定http请求和具体处理函数。
首先,需要了解一些openstack wsgi框架的知识,可以参见http://blog.csdn.net/happyanger6/article/details/54518491。
nova组件执行nova-api时根据api-paste.ini来构建web应用,截取的一段api-paste.ini文件如下:
[composite:osapi_compute]
use = call:nova.api.openstack.urlmap:urlmap_factory
/: oscomputeversions
# v21 is an exactly feature match for v2, except it has more stringent
# input validation on the wsgi surface (prevents fuzzing early on the
# API). It also provides new features via API microversions which are
# opt into for clients. Unaware clients will receive the same frozen
# v2 API feature set, but with some relaxed validation
/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 request_log sizelimit osprofiler noauth2 osapi_compute_app_v21
keystone = cors http_proxy_to_wsgi compute_req_id faultwrap request_log sizelimit osprofiler authtoken keystonecontext osapi_compute_app_v21
...
[app:osapi_compute_app_v21]
paste.app_factory = nova.api.openstack.compute:APIRouterV21.factory
从中可以看到一个/v2.1/***的请求将首先被路由到openstack_compute_api_V21,经过一些filter处理后,最终由/nova/api/openstack/compute下的APIRouterV21类的factory方法处理。所以关于url如何路由到具体函数的确定应该在于APIRouterV21类。
APIRouterV21位于/nova/api/openstack/compute/routes.py下,部分代码如下:
from nova.api.openstack.compute import flavors
# /nova/api/openstack/compute/flavor.py部分代码如下:
class FlavorsController(wsgi.Controller): """Flavor controller for the OpenStack API.""" _view_builder_class = flavors_view.ViewBuilder @validation.query_schema(schema.index_query) @wsgi.expected_errors(400) def index(self, req): """Return all flavors in brief.""" limited_flavors = self._get_flavors(req) return self._view_builder.index(req, limited_flavors) @validation.query_schema(schema.index_query) @wsgi.expected_errors(400) def detail(self, req): """Return all flavors in detail.""" limited_flavors = self._get_flavors(req) req.cache_db_flavors(limited_flavors) return self._view_builder.detail(req, limited_flavors)
def _create_controller(main_controller, controller_list,
action_controller_list):
"""This is a helper method to create controller with a
list of extended controller. This is for backward compatible
with old extension interface. Finally, the controller for the
same resource will be merged into single one controller.
"""
controller = wsgi.Resource(main_controller())
for ctl in controller_list:
controller.register_extensions(ctl())
for ctl in action_controller_list:
controller.register_actions(ctl())
return controller
flavor_controller = functools.partial(_create_controller,
# 如下类中包含detail()方法,查看/nova/api/openstack/compute/flavor.py知道
flavors.FlavorsController,
[],
[
flavor_manage.FlavorManageController,
flavor_access.FlavorActionController
]
)
# NOTE(alex_xu): This is structure of this route list as below:
# (
# ('Route path': {
# 'HTTP method: [
# 'Controller',
# 'The method of controller is used to handle this route'
# ],
# ...
# }),
# ...
# )
#
# Also note that this is ordered tuple. For example, the '/servers/detail'
# should be in the front of '/servers/{id}', otherwise the request to
# '/servers/detail' always matches to '/servers/{id}' as the id is 'detail'.
ROUTE_LIST = (
# NOTE: This is a redirection from '' to '/'. The request to the '/v2.1'
# or '/2.0' without the ending '/' will get a response with status code
# '302' returned.
('/flavors', {
'GET': [flavor_controller, 'index'],
'POST': [flavor_controller, 'create']
}),
# 我们以openstack flavor list为例,当执行这条语句后openstackclient就会发出一个后缀包含/flavors/detail的请求,并且http方法是GET;此时,这个请求就被路由到flavor_controller这个APP
中,具体方法是detail()。关于flavor_controller如何确定,看前一段代码。
('/flavors/detail', {
'GET': [flavor_controller, 'detail']
}),
('/flavors/{id}', {
'GET': [flavor_controller, 'show'],
'PUT': [flavor_controller, 'update'],
'DELETE': [flavor_controller, 'delete']
}),
('/flavors/{id}/action', {
'POST': [flavor_controller, 'action']
}),
('/flavors/{flavor_id}/os-extra_specs', {
'GET': [flavor_extraspec_controller, 'index'],
'POST': [flavor_extraspec_controller, 'create']
}),
('/flavors/{flavor_id}/os-extra_specs/{id}', {
'GET': [flavor_extraspec_controller, 'show'],
'PUT': [flavor_extraspec_controller, 'update'],
'DELETE': [flavor_extraspec_controller, 'delete']
}),
('/flavors/{flavor_id}/os-flavor-access', {
'GET': [flavor_access_controller, 'index']
}),
)
class APIRouterV21(base_wsgi.Router):
"""Routes requests on the OpenStack API to the appropriate controller
and method. The URL mapping based on the plain list `ROUTE_LIST` is built
at here.
"""
def __init__(self, custom_routes=None):
""":param custom_routes: the additional routes can be added by this
parameter. This parameter is used to test on some fake routes
primarily.
"""
super(APIRouterV21, self).__init__(nova.api.openstack.ProjectMapper())
if custom_routes is None:
custom_routes = tuple()
# 将ROUTE_LIST表中的路由规则加载到map中,可见ROUTE_LIST表中包含了路由信息
for path, methods in ROUTE_LIST + custom_routes:
# NOTE(alex_xu): The variable 'methods' is a dict in normal, since
# the dict includes all the methods supported in the path. But
# if the variable 'method' is a string, it means a redirection.
# For example, the request to the '' will be redirect to the '/' in
# the Nova API. To indicate that, using the target path instead of
# a dict. The route entry just writes as "('', '/)".
if isinstance(methods, str):
self.map.redirect(path, methods)
continue
for method, controller_info in methods.items():
# TODO(alex_xu): In the end, I want to create single controller
# instance instead of create controller instance for each
# route.
controller = controller_info[0]()
action = controller_info[1]
self.map.create_route(path, method, controller, action)
@classmethod
def factory(cls, global_config, **local_config):
"""Simple paste factory, :class:`nova.wsgi.Router` doesn't have one."""
return cls()
具体确定方法:1.找到ROUTE_LIST中的url后缀,确定APP方法和ACTION名称;2.根据APP方法和ACTION名称确定具体执行函数。上文中我们就可以知道当openstackclient执行openstack flavor list时,最终会调用到/nova/api/openstack/compute/flavors.py中FlavorsController类的detail()方法。