OpenStack Glance 之paste

OpenStack Glance 之paste


Python paste 是WSGI (web server gateway interface)的一个工具库,Openstack的每个项目基本都用到了该库,本文以Glance在paste上的使用为例来介绍paste。WSGI是web服务与应用之间交互的一种规范,它定义了应用、服务、中间件的概念。
分析过程中用到的glance的配置文件glanc-api-paste.ini,其中内容较多,主要分析如下配置:

[pipeline:glance-api-keystone]
pipeline = cors healthcheck http_proxy_to_wsgi versionnegotiation osprofiler authtoken context  rootapp

[composite:rootapp]
paste.composite_factory = glance.api:root_app_factory
/: apiversions
/v1: apiv1app
/v2: apiv2app

[filter:authtoken]
paste.filter_factory = keystonemiddleware.auth_token:filter_factory
delay_auth_decision = true

-

哪里用到了Paste:


glance-api 服务的入口函数:

def main():
   。。。
        server = wsgi.Server(initialize_glance_store=True)
        #加载glance-api的app
        server.start(config.load_paste_app('glance-api'), default_port=9292)
        server.wait()
    except KNOWN_EXCEPTIONS as e:
        fail(e)

if __name__ == '__main__':
    main()

上面这段代码是glance-api服务端启动的部分代码,服务监听在9292上,app是由config.load_paste_app(‘glance-api’)加载。详细过程如下:

...
from paste import deploy
...

def load_paste_app(app_name, flavor=None, conf_file=None):
#更新app_name,主要根据glance-api.conf配置文件中的paste_deploy片段的flavor配置项,
更新后app_name为glance-api-{flavor}
    app_name += _get_deployment_flavor(flavor)

    if not conf_file:
        conf_file = _get_deployment_config_file()
        ...

        app = deploy.loadapp("config:%s" % conf_file, name=app_name)

        return app

从上面的部分代码可以看出glance-api的app是使用paste.deploy.loadapp(…)加载的。
由此可以知道paste在glance-api服务中是用来加载app的。

  • paste加载app的详细过程:

#同整体代码分析loadapp的参数:confi_file=/etc/glance/glance-api-paste.ini  app_name=glance-api-keystone(在上面的代码中只传入了glance-api服务的名字,为什么现在是glance-api-keystone,表示该服务使用keystone认证功能)
app = deploy.loadapp("config:%s" % conf_file, name=app_name)

paste.deploy.loadapp(“config:/etc/glance/glance-api-paste.ini”, glance-api-keystone)
调用栈:

paste.deploy.loadapp(uri, name=None, **kw)
    >paste.deploy.loadobj(APP, uri, name=name, **kw)
paste.deploy.loadobj(object_type, uri, name=None, relative_to=None, global_conf=None)

def loadobj(object_type, uri, name=None, relative_to=None,
            global_conf=None):
            #load context 
    context = loadcontext(
        object_type, uri, name=name, relative_to=relative_to,
        global_conf=global_conf)
    return context.create()

object_type共有六种:APP(_App) FILTER(_Filter) SERVER(_Server) PIPELINE(_Pipeline) FILTER_APP(_FilterApp) FILTER_WITH(_FilterWith),本文会涉及到APP、FILTER、PIPELINE。

#object_type=APP, uri=config:/etc/glance/glance-api-paste.ini name=glance-api-keystone
def loadcontext(object_type, uri, name=None, relative_to=None,
                global_conf=None):
    if '#' in uri:
        if name is None:
            uri, name = uri.split('#', 1)
        else:
            # @@: Ignore fragment or error?
            uri = uri.split('#', 1)[0]
    if name is None:
        name = 'main'
    if ':' not in uri:
        raise LookupError("URI has no scheme: %r" % uri)
        #scheme=config path=/etc/glance/glance-api-paste.ini
    scheme, path = uri.split(':', 1)
    scheme = scheme.lower()

    if scheme not in _loaders:
        raise LookupError(
            "URI scheme not known: %r (from %s)"
#_loaders={'config': _loadconfig, 'egg': _loadegg, 'call': _loadfunc}
#_loader[scheme] == _loadconfig
    return _loaders[scheme](
        object_type,
        uri, path, name=name, relative_to=relative_to,
        global_conf=global_conf)

字典 _loaders={‘config’: _loadconfig, ‘egg’: _loadegg, ‘call’: _loadfunc}由下面部分代码片段可知。

...
_loaders['config'] = _loadconfig
_loaders['egg'] = _loadegg
_loaders['call'] = _loadfunc
...

_loadconfig(…)详细加载过程

#object_type=APP uri=config:/etc/glance/glance-api-paste.ini path=/etc/glance/glance-api-paste.ini
def _loadconfig(object_type, uri, path, name, relative_to,
                global_conf):
    ...
    loader = ConfigLoader(path)
   ...
    return loader.get_context(object_type, name, global_conf)

ConfigLoader.get_context(…)主要过程:

#object_type=APP name=glance-api-keystone
def get_context(self, object_type, name=None, global_conf=None):
       ...
       #1.获取glance-api-keystone在配置文件中对应的section:pipeline:glance-api-keystone
        section = self.find_config_section(
            object_type, name=name)
        ...
        local_conf = {}
       ...
       #2.解析section pipeline:glance-api-keystone下的配置项:并保持到local_conf字典中。
        for option in self.parser.options(section):
            if option.startswith('set '):
                name = option[4:].strip()
                global_additions[name] = global_conf[name] = (
                    self.parser.get(section, option))
            elif option.startswith('get '):
                name = option[4:].strip()
                get_from_globals[name] = self.parser.get(section, option)
            else:
                if option in defaults:
                    # @@: It's a global option (?), so skip it
                    continue
                local_conf[option] = self.parser.get(section, option)
        ...
        if section.startswith('filter-app:'):
            context = self._filter_app_context(
                object_type, section, name=name,
                global_conf=global_conf, local_conf=local_conf,
                global_additions=global_additions)
                #3.为pipeline创建LoaderContext对象。
        elif section.startswith('pipeline:'):
            context = self._pipeline_app_context(
                object_type, section, name=name,
                global_conf=global_conf, local_conf=local_conf,
                global_additions=global_additions)
        elif 'use' in local_conf:
            context = self._context_from_use(
                object_type, local_conf, global_conf, global_additions,
                section)
        else:
            context = self._context_from_explicit(
                object_type, local_conf, global_conf, global_additions,
                section)
    ...
    #4.返回pipeline LoaderContext对象。
    return context

pipeline LoaderContext的创建过程:

def _pipeline_app_context(self, object_type, section, name,
                              global_conf, local_conf, global_additions):
       ...
       #1.获取pipeline
        pipeline = local_conf.pop('pipeline').split()
        ...
        context = LoaderContext(None, PIPELINE, None, global_conf,
                                local_conf, self)
                                #2为app创建LoaderContext对象
        context.app_context = self.get_context(
            APP, pipeline[-1], global_conf)
            #3.为filter创建LoaderContext对象
        context.filter_contexts = [
            self.get_context(FILTER, name, global_conf)
            for name in pipeline[:-1]]
        return context

从上面的源代码中可以看出,主要做了,key pipeline的value,创建PIPELINE的context对象,创建APP的context对象和FILTER对象。
FILTER Context对象的创建,主要是对实例对象的内部属性的赋值操作:

def __init__(self, obj, object_type, protocol,
                 global_conf, local_conf, loader,
                 distribution=None, entry_point_name=None):
        self.object = obj 
        self.object_type = object_type
        self.protocol = protocol
        #assert protocol in _flatten(object_type.egg_protocols), (
        #    "Bad protocol %r; should be one of %s"
        #    % (protocol, ', '.join(map(repr, _flatten(object_type.egg_protocols)))))
        self.global_conf = global_conf
        self.local_conf = local_conf
        self.loader = loader
        self.distribution = distribution
        self.entry_point_name = entry_point_name

APP Context对象的创建:

        context.app_context = self.get_context(
            APP, pipeline[-1], global_conf)

这里又调用了get_context(…)方法,该方法前文已经介绍,这里不再赘述。该方法根据传入参数的不同,相应的也会走不同的逻辑,APP Context的get_context过程主要如下:
1)寻找app对应的section

        section = self.find_config_section(
            object_type, name=name)

2)把app section下的配置项保存到local_conf字典中

        for option in self.parser.options(section):
            if option.startswith('set '):
                name = option[4:].strip()
                global_additions[name] = global_conf[name] = (
                    self.parser.get(section, option))
            elif option.startswith('get '):
                name = option[4:].strip()
                get_from_globals[name] = self.parser.get(section, option)
            else:
                if option in defaults:
                    # @@: It's a global option (?), so skip it
                    continue
                local_conf[option] = self.parser.get(section, option)

3)调用_context_from_explicit(…)方法创建context对象。

            context = self._context_from_explicit(
                object_type, local_conf, global_conf, global_additions,
                section)

_context_from_explicit(…)的源代码如下:

    def _context_from_explicit(self, object_type, local_conf, global_conf,
                               global_addition, section):
        possible = []
        #查找local_conf中被支持的协议,找到后,把该协议与该协议所对应的value构建成一个元组放入possible列表中
        for protocol_options in object_type.egg_protocols:
            for protocol in protocol_options:
                if protocol in local_conf:
                    possible.append((protocol, local_conf[protocol]))
                    break
                    #检查possible变量
        if len(possible) > 1:
            raise LookupError(
                "Multiple protocols given in section %r: %s"
                % (section, possible))
        if not possible:
            raise LookupError(
                "No loader given in section %r" % section)
        found_protocol, found_expr = possible[0]
        del local_conf[found_protocol]
        #导入协议说对应的value,以为value是python的一个模块、方法或对象,所以可以之间导入。
        value = import_string(found_expr)
        #实例化context对象
        context = LoaderContext(
            value, object_type, found_protocol,
            global_conf, local_conf, self)
        return context

该函数的主要逻辑是:
1)查找local_conf中被支持的协议,找到后,把该协议与该协议所对应的value构建成一个元组放入possible列表中
2)对possible做检查。
3)导入被支持协议对应的value,该value是python支持的类型,本文是glance.api:root_app_factory、
keystonemiddleware.auth_token:filter_factory,即是factory方法。
4)实例化context对象,LoaderContext对象的实例化上文有介绍,不再赘述。
FILTER Context的创建与APP Context的创建过程类似,区别是filter模块有多个需要一个循环,源代码如下:

        context.filter_contexts = [
            self.get_context(FILTER, name, global_conf)
            for name in pipeline[:-1]]

获取context的具体过程,与app context是相似的,这里就不再赘述。
从上文的loadobj(…)的方法可以知道context获取之后就开始调用context.create(…)方法:
context类型是LoaderContext,该类的create的方法如下:

    def create(self):
        return self.object_type.invoke(self)

有源码可知,create方法是调用了LoaderContext属性object_type的invoke(…)方法。
有上文可知object_type的类型有APP(_App) FILTER(_Filter) SERVER(_Server) PIPELINE(_Pipeline) FILTER_APP(_FilterApp) FILTER_WITH(_FilterWith)这六种。这里主要会分析PIPELINE、APP、FILTER。

PIPELIEN.invoke(…)

    def invoke(self, context):
    #创建app
        app = context.app_context.create()
        #创建filter
        filters = [c.create() for c in context.filter_contexts]
        #将filters列表反转
        filters.reverse()
        #filter封装app
        for filter in filters:
            app = filter(app)
        return app

由上面的源码可知:
1)PIPELINE的invoke方法先调用了APP 的create方法,并把返回值赋值给app。
2)常见filter实例,并将filter顺序反转。
3)通过filter来封装app,并返回。

APP.invoke(…)

    def invoke(self, context):
        if context.protocol in ('paste.composit_factory',
                                'paste.composite_factory'):
            return fix_call(context.object,
                            context.loader, context.global_conf,
                            **context.local_conf)
        elif context.protocol == 'paste.app_factory':
            return fix_call(context.object, context.global_conf, **context.local_conf)
        else:
            assert 0, "Protocol %r unknown" % context.protocol

FILTER.invoke(…)

    def invoke(self, context):
        if context.protocol == 'paste.filter_factory':
            return fix_call(context.object,
                            context.global_conf, **context.local_conf)
        elif context.protocol == 'paste.filter_app_factory':
            def filter_wrapper(wsgi_app):
                # This should be an object, so it has a nicer __repr__
                return fix_call(context.object,
                                wsgi_app, context.global_conf,
                                **context.local_conf)
            return filter_wrapper
        else:
            assert 0, "Protocol %r unknown" % context.protocol

有源码可知APP和FILTER的invoke方法都调用了fix_call(…)方法。该方法位于/usr/lib/python2.7/site-packages/paste/deploy/util.py 源码如下:

def fix_call(callable, *args, **kw):
    """
    Call ``callable(*args, **kw)`` fixing any type errors that come out.
    """
    try:
        val = callable(*args, **kw)
    except TypeError:
        exc_info = fix_type_error(None, callable, args, kw)
        reraise(*exc_info)
    return val

有源码可知,fix_call是调用了之前导入的factory方法。以keystonemiddleware.auth_token:filter_factory为例,来分析filter:

def filter_factory(global_conf, **local_conf):
    """Returns a WSGI filter app for use with paste.deploy."""
    conf = global_conf.copy()
    conf.update(local_conf)

    def auth_filter(app):
        return AuthProtocol(app, conf)
    return auth_filter

从上文源码可知,filter_factory复制了一份global_conf的值然后返回一个内部函数实例auth_filter。如果调用该返回的实例,者就会使用参入的参数app,和conf为参数创建一个AuthProtocol实例。
代码分析到这里,我想大家都该清楚了,paste的loadapp的基本过程,即filter列表中的filter,都以倒序的方式对最后一个app进行封装。
- Pipeline模型及app的加载原型
通过上面源代码的分析,建立以下模型:

 [pipeline:xxx]
 pipeline = filter0 filter2 ... filtern app
 [app:yyy]
  paste.app_factory = aaa.bbb.ccc:app_factory
app = loadapp("config:$PATH/zzz.ini", "xxx") ->filter0(filter2(...filtern(app)))

从app的加载过程可以看出在pipeline链上,前面的filter对邻近后面的filter进行封装,最后一个filter对app进行封装。
那么其具体是这么封装的呢?这里以keystonemiddleware.auth_token:filter_factory 对app的封装为例来介绍其封装过程,从上面的源码分析,和app的加载原型课可以看出,filter_factory返回一个auth_filter可执行函数,然后auth_filter被调用,使用app和conf作为参数,创建app被封装后的对象AuthProtocol对象,所以封装就是使用app作为参数来实例化AuthProtocol filter app实例对象。
AuthProtocol实例化过程:

#AuthProtocol(BaseAuthProtocol)
    def __init__(self, app, conf):
        ...
        self._conf = _conf_values_type_convert(conf)
        ...
        super(AuthProtocol, self).__init__(
            app,
            log=log,
            enforce_token_bind=self._conf_get('enforce_token_bind'))
        ...
#BaseAuthProtocol
    def __init__(self,
                 app,
                 log=_LOG,
                 enforce_token_bind=_BIND_MODE.PERMISSIVE):
        self.log = log
        self._app = app
        self._enforce_token_bind = enforce_token_bind

由源码可以知道filter封装的过程就是把参数参入的app赋值给当前实例的_app属性。

既然filter 是wsgi app那么它也一定要满足wsgi的app标准,即:返回的可调用对象 filter app必须是app(environ,start_response)满足这样的调用方式。然而在openstack的一些子项目中除了compsite和app外,filter都没有满足这个要求,那他又是为什么呢?仔细看到的同学会看到每个filter的__call__(…)方法都用了装饰器来修饰,如:

    @webob.dec.wsgify(RequestClass=_request._AuthTokenRequest)
    def __call__(self, req):
        """Handle incoming request."""
        response = self.process_request(req)
        if response:
            return response
        response = req.get_response(self._app)
        return self.process_response(response)

所以可以断定一定是这个装饰器在其中起了作用。那么这个装饰器做了什么呢,让原本的environ和start_response变成了req。进入装饰器中看一下就一目了然了,如下是装饰器的__init__方法:

#注:RequestClass = webob.Request
    def __init__(self, func=None, RequestClass=None,
                 args=(), kwargs=None, middleware_wraps=None):
        self.func = func
        if (RequestClass is not None
            and RequestClass is not self.RequestClass):
            self.RequestClass = RequestClass
        self.args = tuple(args)
        if kwargs is None:
            kwargs = {}
        self.kwargs = kwargs
        self.middleware_wraps = middleware_wraps

    def __call__(self, req, *args, **kw):
        """Call this as a WSGI application or with a request"""
        func = self.func
        if func is None:
            if args or kw:
                raise TypeError(
                    "Unbound %s can only be called with the function it "
                    "will wrap" % self.__class__.__name__)
            func = req
            return self.clone(func)
        if isinstance(req, dict):
            if len(args) != 1 or kw:
                raise TypeError(
                    "Calling %r as a WSGI app with the wrong signature")
            environ = req
            start_response = args[0]
            req = self.RequestClass(environ)
            req.response = req.ResponseClass()
            try:
                args = self.args
                if self.middleware_wraps:
                    args = (self.middleware_wraps,) + args
                resp = self.call_func(req, *args, **self.kwargs)
            except HTTPException as exc:
                resp = exc
            if resp is None:
                ## FIXME: I'm not sure what this should be?
                resp = req.response
            if isinstance(resp, text_type):
                resp = bytes_(resp, req.charset)
            if isinstance(resp, bytes):
                body = resp
                resp = req.response
                resp.write(body)
            if resp is not req.response:
                resp = req.response.merge_cookies(resp)
            return resp(environ, start_response)
        else:
            if self.middleware_wraps:
                args = (self.middleware_wraps,) + args
            return self.func(req, *args, **kw)

该装饰器是一个类,返回的是一个可执行的对象。同以上代码分析可知,该装饰器是一个带参装饰器,当调用被该装饰器修饰的函数时,就会实例化该装饰器实例,然后再调用该可执行实例,参数为AuthProtocol.call(…),最后再次被调用参数为environ和start_response
模型:webob.dec.wsgify(RequestClass=_request._AuthTokenRequest)(\AuthProtocol.call(…))(req, *args, **kw),由此可以看出实际上environ和start_response是直接传给了装饰器实例,那么装饰器实例对其做了什么呢,有如下代码片段可以知道:

    def __call__(self, req, *args, **kw):

...
            environ = req
            start_response = args[0]
            req = self.RequestClass(environ)
            req.response = req.ResponseClass()
...

            resp = self.call_func(req, *args, **self.kwargs)
            ...

    def call_func(self, req, *args, **kwargs):
        """Call the wrapped function; override this in a subclass to
        change how the function is called."""
        return self.func(req, *args, **kwargs)

由上面的源码可知,服务器在接受请求后,调用app时,传递的参数environ和start_response没有变,只是把他传递给了装饰器修饰后的相应方法,该方法对参数做了一层处理,把使用environ作为参数,创建RequestClass(Request)实例,args[0]就是start_response。然后在调用filter的__call__(self, req)方法。

该装饰器在webob包中,webob也是与wsgi相关的工具库。其中Request、Response和Exception对象很重要。这里就不展开讲解。

到这里就清楚了,当server接受到http请求之后来,把请求的一些相关信息都封装在environ对象中,再调用app(environ, start_response)来处理请求,start_response是服务器放的hook,webob对app(environ,start_response)做了拦截,转换成app(req)的请求传递方式,经过多个filter app处理后,再由最后一个app来处理请求。
- filter app在哪里做的过滤操作
我们知道装饰器在做了参数转换之后,交给了filter app来处理,下面以keystonemiddleware.auth_token:filter_factory filter app的处理过程为例,该filter的app是一个AuthProtocol类型的实例,他的父类中实现了__call__(…)方法。

    @webob.dec.wsgify(RequestClass=_request._AuthTokenRequest)
    def __call__(self, req):
        """Handle incoming request."""
        #处理接受到的请求
        response = self.process_request(req)
        if response:
            return response
            #传递到下一个app继续处理
        response = req.get_response(self._app)
        #处理上一个app处理后的结果
        return self.process_response(response)

由源码可知,filter是通过实现process_request和process_response方法来实现对请求和响应的过滤。每个filter因功能的不同,其方法的实现也有所差异,具体过程不再赘述。
request的处理过程如下:
OpenStack Glance 之paste_第1张图片

到这里,好像还没有结束我们pipeline的最后一个app是一个compsite类型的app,不是一个不是一个单纯的app,且还有一些配置项。最后他是怎么转到最终的app上的呢,这里就简单的分析一下。

[composite:rootapp]
paste.composite_factory = glance.api:root_app_factory
/: apiversions
/v1: apiv1app
/v2: apiv2app

[app:apiversions]
paste.app_factory = glance.api.versions:create_resource

[app:apiv1app]
paste.app_factory = glance.api.v1.router:API.factory

[app:apiv2app]
paste.app_factory = glance.api.v2.router:API.factory
def root_app_factory(loader, global_conf, **local_conf):
#v1 v2功能是否启用时可配置的。
    if not CONF.enable_v1_api and '/v1' in local_conf:
        del local_conf['/v1']
    if not CONF.enable_v2_api and '/v2' in local_conf:
        del local_conf['/v2']
    return paste.urlmap.urlmap_factory(loader, global_conf, **local_conf)

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)
    #循环构建不同path路径前缀对应的app
    for path, app_name in local_conf.items():
        path = parse_path_expression(path)
        app = loader.get_app(app_name, global_conf=global_conf)
        urlmap[path] = app
    return urlmap
#URLMap类方法
    def __setitem__(self, url, app):
        if app is None:
            try:
                del self[url]
            except KeyError:
                pass
            return

        dom_url = self.normalize_url(url)
        if dom_url in self:
            del self[dom_url]
            #初始化applications变量
# Make sure applications are sorted with longest URLs first
        self.sort_apps()


#URLMap app
    def __call__(self, environ, start_response):
        host = environ.get('HTTP_HOST', environ.get('SERVER_NAME')).lower()
        if ':' in host:
            host, port = host.split(':', 1)
        else:
            if environ['wsgi.url_scheme'] == 'http':
                port = '80'
            else:
                port = '443'
        path_info = environ.get('PATH_INFO')
        path_info = self.normalize_url(path_info, False)[1]
        #匹配请求的url,找到对应的app处理请求。
        for (domain, app_url), app in self.applications:
            if domain and domain != host and domain != host+':'+port:
                continue
            if (path_info == app_url
                or path_info.startswith(app_url + '/')):
                environ['SCRIPT_NAME'] += app_url
                environ['PATH_INFO'] = path_info[len(app_url):]
                return app(environ, start_response)
        environ['paste.urlmap_object'] = self
        return self.not_found_application(environ, start_response)

通过glance.api:root_app_factory方法的分析可知,他创建的app是URLMap实例,该实例是一个增强性的字典,他提供了对请求的路由处理,根据请求的前缀匹配结果,来获取对应的app,再对请求继续处理。

本文只限于跟踪paste 加载app的过程,对paste中所涉及的概念,由于没有系统了解paste在表述上可能存在偏差,写该博文的出发点是对paste.deploy.loadapp(…)的好奇。

你可能感兴趣的:(openstack)