tacker源码分析(Pike版本)--启动与路由映射

工程地图
对openstack开发稍有了解的人都知道,setup.cfg是整个工程的地图,其中的entry_points段定义了工程的所有入口。在众多entry points中,console_scripts相对比较特殊,这里面的每一条都对应部署环境上的一个可执行命令,安装后位于/usr/local/bin目录中,这些文件是由pbr根据setup.cfg自动生成。来看一下tacker pike版本的console_scripts部分:
[entry_points]
console_scripts =
    tacker-db-manage = tacker.db.migration.cli:main
    tacker-server = tacker.cmd.eventlet.tacker_server:main
    tacker-conductor = tacker.cmd.eventlet.conductor:main
    tacker-rootwrap = oslo.rootwrap.cmd:main
这说明安装后的tacker环境必然包括上面4个可执行命令,确认一下:

发现多了一个tacker命令,其实它是tacker的客户端程序,是安装tacker client生成的,其它三个对应于console_scripts。tacker-conductor是Pike版本新增的,通过监听消息队列TACKER_CONDUCTOR,使用远程RPC来操作tacker;tacker-db-manage使用alembic进行数据库迁移;使用tacker-rootwrap的目的就是针对系统某些特定的操作,让非特权用户以root用户的身份来安全地执行这些操作,据说nova曾经使用sudoers文件来列出允许执行的特权命令,使用sudo来运行这些命令,但是这样做不容易维护,而且不能进行复杂的参数处理;tacker-server是tacker服务的主要入口。

启动流程
以tacker-server为例简单看一下启动流程。进程启动命令:python /usr/local/bin/tacker-server --config-file /etc/tacker/tacker.conf。
按图索骥找到tacker-server对应的python文件,代码库上位于tacker/cmd/eventlet目录下,主要是这个三行代码:
tacker_api = service.serve_wsgi(service.TackerApiService)
launcher = common_service.launch(cfg.CONF, tacker_api, workers=cfg.CONF.api_workers or None)
launcher.wait()
launch函数的第二个参数worker指定worker的数目,如果是1则使用green thread的方式启动服务,大于1的话使用multi process的方式。不论哪种方式,最主要的是启动了TackerApiServer的执行,start时,调用_run_wsgi()方法,这时我们才能确认正在启动的是一个wsgi程序。
_run_wsgi()方法最主要的一个作用就是加载api-paste.ini文件,生成一个wsgi应用程序。

Tacker的启动流程暂时放下,先简单介绍一下早期openstack工程都会用到的几个重要的库:paste、routes和webOb。如果展开讲,这几个库可能就够写几篇文章了,所以这里只是简要介绍一下几个库的基本用法。使用过Flask或者Java的spring写Restful API的同学到这里可能会觉得这几个库有些晦涩,不够fashion,为什么不直接使用annotation将URL和方法映射起来呢,tacker中使用routes做路由配置真的过于复杂,不够灵活。但是好处呢,是api比较规范,层次明确,不会出现些奇形怪状的URL。

Paste Deploy

Openstack使用Paste的Deploy组件构建WSGI服务和应用,每个OpenStack项目都会在/etc/<project>/目录下有一个api-paste.ini文件。仔细看一下文件的内容不难发现,paste实际上构建了api处理的一个pipeline,里面最重要的两个对象是filter和app,二者都是callable object,app是wsgi的application,接受(environ,start_response)作为参数,filter接受一个app作为参数,经过包装,返回给下面的filter或者app。简而言之,一个请求在最终到达app处理时,要经过filter的层层处理,filter有可能直接响应请求,也有可能把经过处理的请求再传递下去。看下面这段:
[composite:tackerapi_v1_0]
use = call:tacker.auth:pipeline_factory
noauth = request_id catch_errors extensions tackerapiapp_v1_0
keystone = request_id catch_errors alarm_receiver authtoken keystonecontext extensions tackerapiapp_v1_0
use调用tacker.auth模块的pipeline_factory方法,读取/etc/tacker/tacker.conf文件中的auth_strategy,选择使用哪个pipeline,默认是keystone。
def pipeline_factory(loader, global_conf, **local_conf):
    """Create a paste pipeline based on the 'auth_strategy' config option."""
    pipeline = local_conf[cfg.CONF.auth_strategy]
    pipeline = pipeline.split()
    filters = [loader.get_filter(n) for n in pipeline[:-1]]
    app = loader.get_app(pipeline[-1]) # pipeline最后一个是app,前面都是filter
    filters.reverse()
    for f in filters:
        app = f(app)                   # filter串行处理,后面一个filter或者app都是前面一个的参数
    return app
最终生成的app对象

可以看到最外层看到的app是RequestId的实例,它包含一个application成员,这个成员是catch_errors的实例,以此类推。

Routes
API请求经过filter的层层处理,来到了extension filter,这时routes模块参与进来。routes模块是python用于url映射的模块,也就是将一个具体的url跟它的处理函数绑定起来。
在Tacker中主要用到map.resource()方法,它会同时将资源的CRUD一次性绑定到一个controller上。比如下面这句
map.resource("message", "messages")
它能够生成下列请求
GET    /messages        => messages.index()    => url("messages")
POST   /messages        => messages.create()   => url("messages")
GET    /messages/new    => messages.new()      => url("new_message")
PUT    /messages/1      => messages.update(id) => url("message", id=1)
DELETE /messages/1      => messages.delete(id) => url("message", id=1)
GET    /messages/1      => messages.show(id)   => url("message", id=1)
GET    /messages/1/edit => messages.edit(id)   => url("edit_message", id=1)
resource方法还有一些其它参数,能够实现更复杂的需求。

webOb
webOb的作用是对wsgi的请求与响应进行封装,来简化wsgi应用的编写。webOb两个最重要的对象,一是webob.Request,对wsgi请求的environ进行封装,一是webob.Response,对标准wsgi响应进行封装。修饰符webob.dec.wsgify用于对原始的wsgi参数和返回进行装饰,让代码更加优雅。
@wsgify
def myfunc(req):
    return webob.Response('Hello world')

上面简单介绍了一下几个关键的库,要想深入理解的话还是要对照代码,来理解一下WSGI是怎么工作的。刚才看到,在请求处理的pipeline中,最后两项是extensions和tackerapiapp_v1_0,一个filter,一个app。
[filter:extensions]
paste.filter_factory = tacker.api.extensions:extension_middleware_factory
[app:tackerapiapp_v1_0]
paste.app_factory = tacker.api.v1.router:APIRouter.factory
tacker.wsgi模块定义了跟WSGI相关的基类,最主要的就是Middleware和Router。paste配置文件中pipeline的filter都继承自Middleware,而最后一个app则继承于Router。按照paste deploy的约定,WSGI中间件要包含一个factory方法,factory方法返回类或者子类的一个实例,另外包含一个__call__方法,实现callable object的目的。
Middleware和Router类有相同的__call__声明,用webOb进行了修饰,但处理逻辑不同,各个filter虽然继承于Middleware,但是可以重新实现__call__方法。
Middleware的__call__方法,通过process_request方法进行过滤处理。
@webob.dec.wsgify
def __call__(self, req):
    response = self.process_request(req)
    if response:
        return response
    response = req.get_response(self.application)
    return self.process_response(response)
Router的__call__方法要基于Routes模块进一步分发处理
@webob.dec.wsgify
def __call__(self, req):
    return self._router
进一步看一下_router
self._router = routes.middleware.RoutesMiddleware(self._dispatch, self.map)
RoutesMiddleware会调用mapper.routematch()方法来获取url的controller等参数,保存在match中,并设置environ变量['wsgiorg.routing_args'],最后会调用其第一个参数给出的函数接口,即self._dispatch。
@staticmethod
@webob.dec.wsgify(RequestClass=Request)
def _dispatch(req):
    match = req.environ['wsgiorg.routing_args'][1]
    if not match:
        return webob.exc.HTTPNotFound()
    app = match['controller']
    return app
其实进一步读代码可以发现,在Tacker中,正常的请求都应该被pipeline中的extensions做出响应,如果最后没有匹配到,才会轮到Router处理,而Router中的map是空的,意味着如果走到Router,那肯定返回404。

Extension/Driver机制
OpenStack项目能够被广泛接纳,一个重要因素是它有很强的extension/driver机制。比如Nova,它能够对接各种不同的虚拟化平台,如xen、kvm、vmsphere等,Neutron能够支持各种不同的二三层网络。
通常来说extension是一种功能扩展,对应于一种业务类型,而driver则用于实现插件或其他所有需要动态加载的场景。Tacker中extension/plugin/drive的关系:比如vnfm extension的具体实现是vnfm plugin,这个plugin又包含3个driver,即infrastructure driver,management driver和monitor driver。
OpenStack的动态加载机制依赖于Stevedore库,有三种方式:driver、hook和extension。driver是根据一个名字加载一个组件, hook是根据一个名字加载多个组件,而extension则是不根据名字,加载namespace下所有组件。
在OpenStack项目中添加新的API或者对现有API进行扩展是件相对容易的事,我们不需要过多关注一个HTTP请求是如何路由到新增资源及其操作上,仅仅需要遵循一些固定的规则与步骤。
extension有3种类型,RequestExtension,ActionExtension,ResourceExtension。resource extension实现一个新的资源,action extension为资源增加动作,request extension能够灵活增加请求。
ExtensionDescriptor是所有extension的基类,定义了extension的公共接口,对应于三种类型分别有get_resources(),get_actions()和get_request_extensions()三个方法。
extension 要遵守一些特定的规则,这些规则如下:
1. extension应该放在tacker/extensions文件夹下,类名和文件名相同,首字母大写。ExtensionManager会在构造时从tacker/extensions目录加载所有扩展。如果不想某个extension被加载,文件名用’_’开头。
2. 扩展extensions.ExtensionDescriptor定义的接口,ExtensionDescriptor中的方法都要重载,否则会加载失败。特别注意get_alias()方法返回正确的名称。重载get_resources()方法创建新资源,这个方法会加载具体plugin,这个就是resource extension。示例代码:
def get_resources(cls):
        resources = list()
        plugin = manager.TackerManager.get_service_plugins()[xxx]
        resource_name = RESOURCE_NAME
        collection_name = COLLECTION_NAME
        params = RESOURCE_ATTRIBUTE_MAP.get(resource_name)
        controller = base.create_resource(collection_name, resource_name, plugin, params, allow_bulk=False)
        ex = extensions.ResourceExtension(collection_name, controller, path_prefix=EXT_PREFIX)
        resources.append(ex)
        return resources
(optional)如果需要扩展资源的属性,重载get_extended_resources(), 返回一个包含新增资源类型及属性的dict,类似RESOURCE_ATTRIBUTE_MAP,新的dict将添加到RESOURCE_ATTRIBUTE_MAP上。这种方式也被视为是request extension。
虽然框架支持action extension和request extension,但是目前Tacker中并没有真正用到,所有请求的处理都通过resource extension。所以这里只要明白二者的目标和实现原理,在具体开发时可以灵活运用。
(optional)request extension
def get_request_extensions(self):
    request_exts = []
    def some_handler():
        pass
    req_ext1 = extensions.RequestExtension('GET', '/dummy_resources/:(id)', some_handler)
    request_exts.append(req_ext1)
    return request_exts
可见request extension在某种程度上游离于OpenStack比较僵化的URL映射机制,特殊情况下可以适度运用。
(optional)action extension
def get_actions(self):
        some_action = extensions.ActionExtension(collection_name, action_name, handler)
        return [some_action]
3. 关联插件和extension。plugin从service_base.NFVPluginBase继承,在supported_extension_aliases中增加支持的extension的别名,如supported_extension_aliases = ['vnfm’],数组类型意味着一个plugin可以支持多个extension。plugin真正实现资源的CURD操作,具体接口可以通过extension的get_plugin_interface获得。

ExtensionMiddleware
回过头来,我们继续看一下在初始化过程中ExtensionMiddleware都做了些啥事?其主要作用就是加载Tacker的各个扩展,如VNFM和NFVO,将各种资源的处理逻辑跟URL映射起来,当请求到来时根据URL中的资源和action信息找到对应的方法。ExtensionMiddleware重写了_call__()方法,同样使用RoutesMiddleware来分发请求,换句话说,ExtensionMiddleware一堆复杂的逻辑就是为了初始化mapper对象,然后构造RoutesMiddleware。贴段代码:
def __init__(self, application, ext_mgr=None):
    self.ext_mgr = (ext_mgr or ExtensionManager(get_extensions_path()))
    mapper = routes.Mapper()

    # extended resources
    for resource in self.ext_mgr.get_resources():
        path_prefix = resource.path_prefix
        if resource.parent:
            path_prefix = (resource.path_prefix + "/%s/{%s_id}" %  (resource.parent["collection_name"], resource.parent["member_name"]))
        for action, method in (resource.collection_actions).items():
            conditions = dict(method=[method])
            path = "/%s/%s" % (resource.collection, action)
            with mapper.submapper(controller=resource.controller, action=action, path_prefix=path_prefix, conditions=conditions) as submap:
                submap.connect(path)
                submap.connect("%s.:(format)" % path)
        mapper.resource(resource.collection, resource.collection, controller=resource.controller, member=resource.member_actions, parent_resource=resource.parent, path_prefix=path_prefix)

    # extended actions
    action_controllers = self._action_ext_controllers(application, self.ext_mgr, mapper)
    for action in self.ext_mgr.get_actions():
        controller = action_controllers[action.collection]
        controller.add_action(action.action_name, action.handler)

    # extended requests
    req_controllers = self._request_ext_controllers(application, self.ext_mgr, mapper)
    for request_ext in self.ext_mgr.get_request_extensions():
        controller = req_controllers[request_ext.key]
        controller.add_handler(request_ext.handler)

    self._router = routes.middleware.RoutesMiddleware(self._dispatch, mapper)
    super(ExtensionMiddleware, self).__init__(application)
ExtensionMiddleware的初始化函数看似非常复杂,但是按照注释拆一下,思路就比较清晰了。
1.前两句话分别是加载所有的extension并构造一个空的mapper
2.调用每个extension的get_resources方法,扩展资源
3.调用每个extension的get_actions方法,扩展action
4.调用每个extension的get_request_extensions方法,扩展request
5.最后,将初始化完成的mapper传给RoutesMiddleware

不得不再多说一句的是各个extension模块中都有的RESOURCE_ATTRIBUTE_MAP结构,它是extension资源的实际定义者,非常重要,如果在REST请求时出错,比如报属性非法,或者只读,那就是RESOURCE_ATTRIBUTE_MAP的问题。其结构为map[<resource_name>][<attribute_name>][<attribute_property>]。

比如下面这个定义,位于vnf资源内
'vnfd_id': {
    'allow_post': True,
    'allow_put': False,
    'validate': {'type:uuid': None},
    'is_visible': True,
    'default': None
}
这意味着在vnf操作中,create时可以带上vnfd_id,如果没有default,那vnfd_id就是必填项,update时不允许带vnfd_id,在show vnf时会返回vnfd_id。 


下图是Tacker中主要模块、类的关系图,不完整,希望对理解有帮助

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值