neutron WSGI application 加载过程

Posted by dodoYuan on February 17, 2018

1、WSGI接口介绍

进入正题之前,先复习下WSGI

WSGI的出发点:底层代码由专门的服务器软件实现,我们用Python专注于生成HTML文档。因为我们不希望接触到TCP连接、HTTP原始请求和响应格式,所以,需要一个统一的接口,让我们专心用Python编写Web业务。

WSGI接口定义非常简单,它只要求Web开发者实现一个函数,就可以响应HTTP请求。我们来看一个最简单的Web版本的“Hello, web!”:

def application(environ, start_response):
    start_response('200 OK', [('Content-Type', 'text/html')])
    return '<h1>Hello, web!</h1>'

上面的application()函数就是符合WSGI标准的一个HTTP处理函数,它接收两个参数: environ:一个包含所有HTTP请求信息的dict对象; start_response:一个发送HTTP响应的函数。 在application()函数中,调用:

start_response('200 OK', [('Content-Type', 'text/html')])

就发送了HTTP响应的Header,注意Header只能发送一次,也就是只能调用一次start_response()函数。start_response()函数接收两个参数,一个是HTTP响应码,一个是一组list表示的HTTP Header,每个Header用一个包含两个str的tuple表示。 通常情况下,都应该把Content-Type头发送给浏览器。其他很多常用的HTTP Header也应该发送。 然后,函数的返回值’<h1>Hello, web!</h1>‘将作为HTTP响应的Body发送给浏览器。

简单总结下,WSGI将Web组件分为三类:

  • 服务器(WSGI Server),负责接收HTTP请求,并封装一系列的环境变量。
  • 中间件(WSGI Middleware),负责将HTTP请求路由给不同的应用对象,并将处理结果返回给WSGI Server。从服务端看,中间件就是个WSGI应用,而从应用端看,中间件相当于服务端。
  • 应用程序(WSGI Application),是可被调用的Python对象,一般接收两个参数,environ和start_response(回调函数)。

当一个请求发送(转发)到WSGI Server时,Server会为应用端准备上下文信息和回调函数,应用端处理完后,便使用提供的回调函数返回相应的请求结果。其中中间件则作为服务端和应用端之间交互的一种包装,经过不同的中间件,便具有不同的功能,如URL分发、权限认证等,不同中间件的组合形成和WSGI的框架。虽然感觉很复杂,但Openstack中使用Paset的Deploy组件来完成WSGI服务器和应用的构建,这一切就变得简单多了。

一个简单的WSGI示意:

# server.py
# 从wsgiref模块导入:
from wsgiref.simple_server import make_server
# 导入我们自己编写的application函数:
from hello import application

# 创建一个服务器,IP地址为空,端口是8000,处理函数是application:
httpd = make_server('', 8000, application)
print "Serving HTTP on port 8000..."
# 开始监听HTTP请求:
httpd.serve_forever()

然后就可以在服务器端打开相应的界面。 以上参考廖雪峰老师WSGI接口

2、paste deploy相关

paste deploy是用来发现和配置WSGI应用的一套系统,对于WSGI应用的使用者而言,可以方便地从配置文件汇总加载WSGI应用(loadapp);对于WSGI应用的开发人员而言,只需要给自己的应用提供一套简单的入口点即可。 PasteDeploy配置文件很关键,由若干section组成,section的声明格式如下: ` [type:name] ` 其中,方括号括起的section声明一个新section的开始,section的声明由两部分组成,section的类型(type)和section的名称(name),如:[app:main]等。section的type可以有:app、composite、filter、pipeline、filter-app等。 每个section中具体配置项的格式就是基本的ini格式: key = value ,所有从PasteDeploy配置文件中提取的参数键、值都以字符串的形式传入底层实现。 简单示例:

   [composite:main]
  use = egg:Paste#urlmap
  / = home
  /blog = blog
  /wiki = wiki
  /cms = config:cms.ini

  [app:home]
  use = egg:Paste#static
  document_root = %(here)s/htdocs

  [app:wiki] 
  use = call:mywiki.main:application 
  database = sqlite:/home/me/wiki.db

  [filter-app:blog] 
  use = egg:Authentication#auth 
  next = blogapp 
  roles = admin 
  htpasswd = /home/me/users.htpasswd 

  [app:blogapp]
  use = egg:BlogApp
  database = sqlite:/home/me/blog.db

  [app:main]
  use = egg:MyEgg
  filter-with = printdebug

  [filter:printdebug] 
  use = egg:Paste#printdebug 

  [pipeline:main]
  pipeline = filter1 filter2 filter3 app
  ...
  • Type = composite(组合应用),组合应用由若干WSGI应用组成,composite为这些应用提供更高一层的分配工作。Composite类型的section将URL请求分配给其他的WSGI应用。 use = egg:Paste#urlmap 意味着使用 Paste 包中的 urlmap 应用。urlmap是Paste提供的一套通用的composite应用,作用就是根据用户请求的URL前缀,将用户请求映射到对应的WSGI应用上去。这里的WSGI应用有:”home”, “blog”, “wiki” 和 “config:cms.ini”。
  • app类型的section声明一个具体的WSGI应用。调用哪个python module中的app代码则由的use后的值指定。这里的 egg:Paste#static 是另一个简单应用,作用仅仅是呈现静态页面。它接收了一个配置项: document_root ,后面的值可以从全局配置DEFAULT(大小写敏感)中提取,提取方法s是使用变量替换:比如%(var_name)s 的形式。这里 %(here)s 的意思是这个示例配置文件所在的目录。

参考 https://tommylike.github.io/openstack/2016/08/03/Paste-Deploy-Introduce/

3、neutron api-paste.ini配置文件加载app分析

这里稍微深入理解下WSGI应用加载过程,也就是load_paste_app(app_name)函数,loader = wsgi.Loader(cfg.CONF)和app = loader.load_app(app_name) 相关源码超出了neutron的范围,在OSLO的server模块中,关键代码如下:

class Loader(object):
    """Used to load WSGI applications from paste configurations."""

    def __init__(self, conf):
        """Initialize the loader, and attempt to find the config.
        :param conf: Application config
        :returns: None
        """
        conf.register_opts(_options.wsgi_opts)
        self.config_path = None

        config_path = conf.api_paste_config
        if not os.path.isabs(config_path):
            self.config_path = conf.find_file(config_path)
        elif os.path.exists(config_path):
            self.config_path = config_path

        if not self.config_path:
            raise ConfigNotFound(path=config_path)

    def load_app(self, name):
        """Return the paste URLMap wrapped WSGI application.
        :param name: Name of the application to load.
        :returns: Paste URLMap object wrapping the requested application.
        :raises: PasteAppNotFound
        """
        try:
            LOG.debug("Loading app %(name)s from %(path)s",
                      {'name': name, 'path': self.config_path})
            return deploy.loadapp("config:%s" % self.config_path, name=name)
        except LookupError:
            LOG.exception("Couldn't lookup app: %s", name)
            raise PasteAppNotFound(name=name, path=self.config_path)

deploy.loadapp(“config:%s” % self.config_path, name=name)是关键,就是从一个paste config file中构建一个WSGI application。config_path = conf.api_paste_config,可以定位到etc/api-paste.ini文件。

[composite:neutron]
use = egg:Paste#urlmap
/: neutronversions_composite
/v2.0: neutronapi_v2_0

[composite:neutronapi_v2_0]
use = call:neutron.auth:pipeline_factory
noauth = cors http_proxy_to_wsgi request_id catch_errors extensions neutronapiapp_v2_0
keystone = cors http_proxy_to_wsgi request_id catch_errors authtoken keystonecontext extensions neutronapiapp_v2_0
...

不去深究deploy.loadapp实现,但我们知道api-paste.ini文件对应的是一个个WSGI application,通过事先约定好的规则,对HTTP的一系列URL请求进行分发到对应的app上。 [composite:neutron]通过urlmap应用提取URL特征,以’/’开头的交由neutronversions_composite处理,以‘/v2.0’开头的url交由neutronapi_v2_0处理。这两个section都涉及到pipeline,我们对[composite:neutronapi_v2_0]做详细分析。 [composite:neutronapi_v2_0]使用neutron.auth:pipeline_factory函数处理http请求,以key/value字典形式传入noauth和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])
    filters.reverse()
    for filter in filters:
        app = filter(app)
    return app

cfg.CONF.auth_strategy中设定的是keystone,因此pipeline = ‘cors http_proxy_to_wsgi request_id catch_errors authtoken keystonecontext extensions neutronapiapp_v2_0’,filters = [loader.get_filter(n) for n in pipeline[:-1]]这句话通过调用loader,获取一系列的filter,注意到pipeline 中其实是一些列section name,即

[filter:request_id]
paste.filter_factory = oslo_middleware:RequestId.factory

[filter:catch_errors]
paste.filter_factory = oslo_middleware:CatchErrors.factory

[filter:cors]
paste.filter_factory = oslo_middleware.cors:filter_factory
oslo_config_project = neutron

[filter:http_proxy_to_wsgi]
paste.filter_factory = oslo_middleware.http_proxy_to_wsgi:HTTPProxyToWSGI.factory

所以loader.get_filter会将pipeline中存储的所有filter type的section所对应的具备filter特征的WSGI application加载。 app = loader.get_app(pipeline[-1])加载最后一个app,也就是 app = neutronapiapp_v2_0,接下来将所有的filter倒序排列,后面三句就是将app一层一层加上过滤条件,首先加上的是extensions ,然后是keystonecontext,最后是cors ,这也是上面需要翻转的原因,因为首先要加上最内层的filter。

4、WSGI整体流程分析

梳理整个流程,在前面web server启动流程我们分析过最终通过协程运行的是如下函数

    def _run(self, application, socket):
        """Start a WSGI server in a new green thread."""
        eventlet.wsgi.server(socket, application,
                             max_size=self.num_threads,
                             log=LOG,
                             keepalive=CONF.wsgi_keep_alive,
                             log_format=CONF.wsgi_log_format,
                             socket_timeout=self.client_socket_timeout)

短短一行代码就构建了一个包含WSGI application的web server,当一个http到达这个web server,先经过urlmap处理,如果是以‘/2.0’开头,那么在交给cors、http_proxy_to_wsgi,一步一步传递到 extensions ,最后再交给neutronapiapp_v2_0处理。 所以说ini配置文件很强大,实现了对URL的分发以及对处理流程pipeline化。 但上述还只是机制上的一些处理,没有说明具体应用是如何运行的。坐稳咯,开始出发了~ 我们先看最关键的,也就是neutronapiapp_v2_0 app的实现。

[app:neutronapiapp_v2_0]
paste.app_factory = neutron.api.v2.router:APIRouter.factory

如上,函数定位到:

def APIRouter(**local_config):
    return pecan_app.v2_factory(None, **local_config)

def _factory(global_config, **local_config):
    return pecan_app.v2_factory(global_config, **local_config)

setattr(APIRouter, 'factory', _factory)

上来就很晕,定位函数为

def v2_factory(global_config, **local_config):
    # Processing Order:
    #   As request enters lower priority called before higher.
    #   Response from controller is passed from higher priority to lower.
    app_hooks = [
        hooks.UserFilterHook(),  # priority 90
        hooks.ContextHook(),  # priority 95
        hooks.ExceptionTranslationHook(),  # priority 100
        hooks.BodyValidationHook(),  # priority 120
        hooks.OwnershipValidationHook(),  # priority 125
        hooks.QuotaEnforcementHook(),  # priority 130
        hooks.NotifierHook(),  # priority 135
        hooks.QueryParametersHook(),  # priority 139
        hooks.PolicyHook(),  # priority 140
    ]
    app = pecan.make_app(root.V2Controller(),
                         debug=False,
                         force_canonical=False,
                         hooks=app_hooks,
                         guess_content_type_from_ext=True)
    startup.initialize_all()
    return app

所以pecan.make_app还得了解下,这个函数返回了最终的APP,因此可以猜测pecan.make_app肯定实现了请求和相应的处理逻辑。 http://huntxu.github.io/2015-12-03-neutron-in-half-an-hour.html