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
-
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的。
#同整体代码分析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的处理过程如下:
到这里,好像还没有结束我们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(…)的好奇。