PasteDeploy 模块学习总结

人生在世,有得就有失,有付出就有回报,鱼和熊掌不能兼得。有时你的付出不一定能得到回报,但自己要想明白一些,不要太苛求自己,生命总有它的轮回,上帝是公平的,它对每个人都是一样的垂青。人生苦短,就好好的潇洒走一回。

简介

官方文档把PasteDeploy介绍为一个发现并配置WSGI应用和服务的一个系统,通过loadapp(一个简单的函数)就可以部署WSGI,而且不需要知道WSGI应用的细节。

我们先来看下,在WSGI简单的模型中,如果有多个URL,一般是怎么做的呢?直接在代码判断请求方和请求路径的然后分别处理不同的URL。

处理函数 app.py

1
2
3
4
5
6
7
def application(environ, start_response):
method = environ['REQUEST_METHOD'] #从环境变量中提取请求方法
path = environ['PATH_INFO'] #从环境变量中提取请求路径
if method=='GET' and path=='/': #通过判断的方法来确定处理的函数
return <h1>path = /</h1> #该函数对应的URL是 "/"
if method=='POST' and path='/signin':
return <h1>path = /signin </h1> #该函数对应URL的是 "/sigin"

服务器函数 server.py

1
2
3
4
5
6
7
8
9
10
# 从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()

这种方法是简单,直接的,但也是可重复性差,不可维护的。一个restful中有可能50个以上的URL路径,如果这么写下去,那维护的人要抓狂。

所以要以一种简单清晰,可维护性好的方式去解析URL,这种方式便是使用PasteDeploy模型。

PasteDeploy的工作模式是使用一个配置文件configure.ini去解析URL。

paste 配置分析

PasteDeploy配置文件由若干section组成,section的声明格式如下:

1
[type:name]

其中,方括号括起的section声明一个新section的开始,section的声明由两部分组成,section的类型(type)和section的名称(name),如:[app:main]等。section的type可以有:app、composite、filter、pipeline、filter-app等。

每个section中具体配置项的格式就是基本的ini格式:key = value,此外,PasteDeploy的配置文件中使用“#”标注注释。

在基本了解PasteDeploy配置文件的书写格式后,我们不妨看一个实例,来具体了解不同type的section。

先从一个PasteDeploy的配置文件(config.ini)入手分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
[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

[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:wiki]
use = call:mywiki.main:application
database = sqlite:/home/me/wiki.db

上面的示例文件列出了若干不同type的section示意,下面就一一探讨PasteDeploy可定义的section type。

Type = composite(组合应用)

顾名思义,组合应用由若干WSGI应用组成,composite为这些应用提供更高一层的分配工作。

我们具体分析示例1中的如下部分:

1
2
3
4
5
6
[composite:main]
use = egg:Paste#urlmap
/ = home
/blog = blog
/wiki = wiki
/cms = config:cms.ini

该段配置文件定义了一个名为main、类型为composite的section,方括号的声明以下是该section的具体配置,遵循 key = value 的统一格式。

Composite类型的section将URL请求分配给其他的WSGI应用。

use = egg:Paste#urlmap 意味着使用 Paste 包中的 urlmap 应用。urlmap是Paste提供的一套通用的composite应用,作用就是根据用户请求的URL前缀,将用户请求映射到对应的WSGI应用上去。这里的WSGI应用有:“home”, “blog”, “wiki” 和 “config:cms.ini”。

最后一项仅仅是参考了同一个目录中的另一个文件”cms.ini”。

Type = app(WSGI应用)

回到示例1中的下一部分:

1
2
3
4
5
6
7
[app:home]
use = egg:Paste#static
document_root = %(here)s/htdocs

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

app类型的section声明一个具体的WSGI应用。调用哪个python module中的app代码则由的use后的值指定。

这里的 egg:Paste#static 是另一个简单应用,作用仅仅是呈现静态页面。它接收了一个配置项: document_root ,后面的值可以从全局配置DEFAULT中提取,提取方法s是使用变量替换:比如 %(var_name)s 的形式。

这里 %(here)s 的意思是这个示例配置文件所在的目录,因为相对路径在不同服务器中的解释方法不同,出于移植性的考虑,官方文档上推荐当前这种写法。

示例中定义了多个app类型的section,因为PasteDeploy的配置文件中允许定义多个app类型的section,同时要求每个WSGI应用也都应该拥有自己的section。这样,每一个WSGI应用在配置文件中都有一个app类型的section与之对应,默认地,”main”应用对应于 app:main 或 app 。

应用的具体实现要在section中配置,有两种方法专门用于指出应用对应的代码:使用URI(用use标识)或 直接指向实现代码(用protocol标识)。

使用另一个URI

采用该方法的特点是指出应用的实现代码的那一条 key = value 配置项采用”use”作为键,该方法也有许多变种,官方的示例中给出了一些介绍:

1
2
3
4
5
6
7
8
9
10
11
12
[app:myapp]
use = config:another_config_file.ini#app_name

# 或任意URI:
[app:myotherapp]
use = egg:MyApp
# 或指明某个模块中的可调用:
[app:mythirdapp]
use = call:my.project:myapplication
# 甚至是其他的section:
[app:mylastapp]
use = myotherapp

最后指向其他的section的那个例子,看起来似乎没有什么意义,似乎只是两个相同的WSGI应用。但是这样的定义允许我们在 [app:mylastapp] 这个应用中定义一些局部的配置项,从而在重用代码的同时覆写它引用的应用配置。

直接指向应用的实现代码

采用该方法的特点是指出实现代码的那一条 key = value 配置项采用“协议”作为键,所谓“协议”即protocol,告诉PasteDeploy即将加载的对象类型,如:

1
2
[app:myapp]
paste.app_factory = myapp.modulename:app_factory

该例的protocol paste.app_factory 是一个应用的工厂函数,指明import对象的类型;值 myapp.modulename:app_factory 指明具体加载的模块和方法。

除了app_factory外还有composite_facory,fliter_factory, fliter_app_factory,server_factory,server_runner等,后面跟上myapp.modulename模块下的app_factory类,不同协议其app_factory也不一样。

那么这几种工厂模式,配置了之后在代码中怎么使用呢?

实现factory

app_factory

1
2
def app_factory(global_config, **local_conf):
return wsgi_app

global_config传入的是字典参数,local_conf传入的是key-value参数,返回一个wsgi application

composite_factory

1
2
def composite_factory(loader, global_config, **local_conf):
return wsgi_app

loader调用一些特殊函数, get_app(name_or_uri, global_conf=None) return a WSGI application with the given name. get_filter and get_server work the same way。如下使用方案:

1
2
3
4
5
6
7
8
9
def pipeline_factory(loader, global_config, pipeline):
# space-separated list of filter and app names:
pipeline = pipeline.split()
filters = [loader.get_filter(n) for n in pipeline[:-1]]
app = loader.get_app(pipeline[-1])
filters.reverse() # apply in reverse order!
for filter in filters:
app = filter(app)
return app
1
2
3
4
5
6
7
8
9
10
[composite:main]
use = <pipeline_factory_uri>
pipeline = egg:Paste#printdebug session myapp

[filter:session]
use = egg:Paste#session
store = memory

[app:myapp]
use = egg:MyApp

将数据读取传入,将filter printdebug,session读入并付给filter,读取app为pipeline参数的最后一个myapp,翻转filter先运行session(myapp),再运行egg:Paste#printdebug(myapp)。

fliter_factory

类似app_factroy但返回的是filter对象,而且仅仅接受WSGI application为唯一的参数,如下所示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def auth_filter_factory(global_conf, req_usernames):
# space-separated list of usernames:
req_usernames = req_usernames.split()
def filter(app):
return AuthFilter(app, req_usernames)
return filter

class AuthFilter(object):
def __init__(self, app, req_usernames):
self.app = app
self.req_usernames = req_usernames

def __call__(self, environ, start_response):
if environ.get('REMOTE_USER') in self.req_usernames:
return self.app(environ, start_response)
start_response(
'403 Forbidden', [('Content-type', 'text/html')])
return ['You are forbidden to view this resource']

定义了AuthFilter对象实现请求变量’REMOTE_USER’下的变量和req_usernames是否一致,filter(app)调用唯一对象app,再里面再做处理并返回信息,通过call把类像函数一样调用。

fliter_app_factory

1
2
3
class AuthFilter(object):
def __init__(self, app, global_conf, req_usernames):
....

除了参数包括app,返回对象为WSGI application外,其他不变。

server_factory

一个参数 wsgi_app,返回serve

1
2
3
4
5
6
def server_factory(global_conf, host, port):
port = int(port)
def serve(app):
s = Server(app, host=host, port=port)
s.serve_forever()
return serve

server_runner

官方文档说不传wsgi_app,其他和server_factory一样,没给例子

Type = filter*(过滤器)

filter是作用于WSGI应用上的函数或方法,以app为唯一的参数,并返回一个“过滤”后的app。归功于WSGI接口的规范,不同的filter可以依次“过滤”某一app,事实上多个filter处理一个app也就是下文中提到的管道(pipeline)。

主要有三种方法进行filter,第一种是fliter-with,第二种是filter-app,第三种是pipeline,多说一句,openstack用的最多的是pipeline。

Type = filter(过滤器)

第一种,通过fliter-with指向下一个fliter应用

在PasteDeploy的配置文件中有多种方法来“过滤”应用,比如示例1中:

1
2
3
4
5
6
7
[app:main]
use = egg:MyEgg
filter-with = printdebug

[filter:printdebug]
use = egg:Paste#printdebug
# and you could have another filter-with here, and so on...

在 [app:main] 的 filter-with 字段指明用来处理该应用的filter,就指定了名为”printdebug”的filter来处理应用”main”。在 [filter:printdebug] 中还可以定义新的 filter-with 字段,从而将处理关系延续下去。

Type = filter-app

第二种,通过next配置指向下一个filter应用

同样是处理应用,在PasteDeploy配置文件中可以有着不同的写法,比如示例1中的下面部分,就是使用filter-app类型的section来声明一个filter:

1
2
3
4
5
6
7
8
9
[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

该部分采用了[filter-app:NAME]类型的section声明了一个filter,指定使用的代码,以及要处理的应用: next 字段的值。从而PasteDeploy会自动地将过滤器”blog”作用在应用”blogapp”上。

Type = pipeline

第三种,pipeline依次列出filter即可

pipeline便于对一个应用添加多个过滤器,比如示例1中:

1
2
3
4
5
[pipeline:main]
pipeline = filter1 egg:FilterEgg#filter2 filter3 app

[filter:filter1]
...

就指定了在app上施加三个filter进行处理。

总结起来,想要在某个应用前添加多个filter,共有 [filter-app:…] [pipeline:…] 和 [app:…] filter-with = … 等方法。

egg模式,通过setuptools打包python源文件,类似java jar的格式;

1
2
3
4
5
6
7
8
9
10
> setup(
> name='MyApp',
> ...
> entry_points={
> 'paste.app_factory': [
> 'main=myapp.mymodule:app_factory',
> 'ob2=myapp.mymodule:ob_factory'],
> },
> )
>

>

通过上面配置安装egg,use=egg:MyApp#main或者use=egg:MyApp#ob2,可以分别找到myapp.mymodule下的app_factory对象和ob_factory对象

局部配置与全局配置

局部配置与全局配置的格式

PasteDeploy配置文件的所有配置项均使用 key = value 格式,但是局部配置项和全局配置项定义的位置不同。如:

1
2
3
4
5
6
7
8
[app:blog]
use = egg:MyBlog
database = mysql://localhost/blogdb
blogname = This Is My Blog!

[app:otherblog]
use = blog
blogname = The other face of my blog

每一个section内设置具体的键值关系,构成这些section自己的局部配置。

为了便于不同的应用读取某些固定的系统信息,PasteDeploy允许设置全局配置变量,所有的全局配置必须放在[DEFAULT]字段下设置,如:

1
2
3
4
5
6
[DEFAULT]
admin_email = webmaster@example.com

[app:main]
use = ...
set admin_email = bob@example.com

注意[DEFAULT]段名是大小写敏感的,因此必须严格大写。

局部配置和全局配置的覆写

上面的两个例子,实际上展示了局部配置和全局配置的覆写,这里详细介绍,首先看局部配置的覆写:

1
2
3
4
5
6
7
8
[app:blog]
use = egg:MyBlog
database = mysql://localhost/blogdb
blogname = This Is My Blog!

[app:otherblog]
use = blog
blogname = The other face of my blog

已经知道,一些section可以直接复用其他section的代码,并定制配置信息,这里 [app:otherblog] 就采用了 [app:blog] 的代码,同时将配置项 blogname 改为自己特定的。

另一方面,应用在本地可以修改全局配置项的值:

1
2
3
4
5
6
[DEFAULT]
admin_email = webmaster@example.com

[app:main]
use = ...
set admin_email = bob@example.com

只需要在要覆写的键前加 set 即可。

至于为什么要探讨局部配置与全局配置,是因为二者在传递给不同类型的factory function时对应的参数不同,这些将在下文详细探讨。

paste 使用

使用示例 1

借鉴一个例子来讲解,原文 http://blog.csdn.net/li_101357/article/details/52755367

在家里水系统的模型大概如下图,以及对应的模拟路径:

img

1
2
3
总开关      /main
淋浴器 /main/boiler/shower
水龙头 /main/tap

PasteDeploy模型中大致可分部件,分别是:

imgimg

imgimg

对应到上例:

  1. app 水龙头 淋浴器
  2. filter 热水器
  3. pipeline 热水器 + 淋浴器
  4. composite 总开关

确定了对应的URL之后,使用PasteDeploy组件来构造解析文件configure.ini:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
[composite:main]
use = egg:Paste#urlmap
/main/tap = tap
/main/boil/shower = pip_to_shower

[app:tap]
paste.app_factory = tap:app_factory
in_arg = water

[pipeline:pip_to_shower]
pipeline = boiler shower

[filter:boiler]
paste.filter_app_factory = boiler:filter_app_factory
in_arg = water

[app:shower]
paste.app_factory = shower:app_factory
in_arg = hot_water

我们再一一分析下,顺便复习一下:

1
2
3
4
[composite:main]
use = egg:Paste#urlmap
/main/tap = tap
/main/boil/shower = pip_to_shower

Paste#urlmap 表示,默认使用Paste.urlmap。

use = egg:Paste#urlmap 意味着直接使用来自于Paste包的urlmap的composite应用。 urlmap是特别常见的composite应用——它使用路径前缀来映射将你的请求与其他应用对应起来。

基本含义就是说,这是Paste已经提供好的一个composite,如果你想自定义就需要另外写一个composite_factory了。

1
2
3
[app:tap]
paste.app_factory = tap:app_factory
in_arg = water

表示路径”/tap”的处理方法paste.app_factory存在于tap.py文件的的app_factory中,这是一个方法。

1
2
[pipeline:pip_to_shower]
pipeline = boiler shower

pipeline 主要起到组合的作用,将filter(过滤器)和app(应用)组合起来,形成一条管道。

1
2
3
[filter:boiler]
paste.filter_app_factory = boiler:filter_app_factory
in_arg = water

filter类似app,只不过换成了paste.filter_app_factory,filter首先执行过滤功能,然后执行app。

配置文件中将路径的处理都配置好:

/main/tap 路径对应的处理函数 tap ,tap是文件tap.py的app_factory方法。

/main/boil/shower 路径对应的处理函数是管道 pip_to_shower 。管道由过滤器 boiler和应用shower组成。首次经过boiler的过滤,然后调用shower函数处理。

下面完成tap、shower和filter文件:

tap.py

1
2
3
4
5
6
7
8
9
10
11
class Tap(object): 
def __init__(self, in_arg):
self.in_arg = in_arg

def __call__(self, environ, start_response):
print 'Tap'
start_response('200 ok', [('Content-Type', 'text/html')])
return "<h1> Tap! </h1>"

def app_factory(global_config, in_arg):
return Tap(in_arg)

app_factory是tap对应的处理方法,返回时调用了Tap方法,Tap对应的是类Tap的call方法,在该方法中打印一个”Tap”,然后发送报文头,最后返回一个字符串。

shower.py

1
2
3
4
5
6
7
8
9
10
11
class Shower(object): 
def __init__(self, in_arg):
self.in_arg = in_arg

def __call__(self, environ, start_response):
print 'Shower'
start_response('200 ok', [('Content Type', 'text/html')])
return "<h1> Shower! </h1>"

def app_factory(global_config, in_arg):
return Shower(in_arg)

shower的分析同上

boiler.py

1
2
3
4
5
6
7
8
9
10
class Boiler(object): 
def __init__(self, app, in_arg):
self.app = app
self.in_arg = in_arg

def __call__(self, environ, start_response):
print 'Boiler'
return self.app(environ, start_response)
def filter_app_factory(app, global_config, in_arg):
return Boiler(app, in_arg)

filter_app_factory是boiler对应的处理方法,其中传入的参数中有一个app,返回时调用了Boiler,并传入参数app。

Boiler是类Boiler的call方法,首先打印了字符串”Boiler”,然后返回时调用了函数app。这个app具体到本例就是调用了shower

所有的文件都准备齐全了,接下来开启WSGI服务,让程序跑起来。

server.py

1
2
3
4
5
6
7
8
9
10
11
12
from wsgiref.simple_server import make_server 
from paste import httpserver
from paste.deploy import loadapp
import os

if __name__ == '__main__':
configfile = 'configure.ini' #定义配置文件
appname = 'main' #composite的名称
wsgi_app = loadapp('config:%s' % os.path.abspath(configfile), appname) #载入配置文件
     print "start the server listening on 8080"
server = make_server('0.0.0.0', 8080, wsgi_app)
server.serve_forever()

运行server程序。

img

在浏览器中请求URL http://[ip ]:8080/main/tap

img

在浏览器中请求URL http://[ip ]:8080/main/boiler/shower

img

小结 1

使用PasteDeploy模块将URL解析从判断的方式转变到文件配置的方式。

使用configuer.ini文件配置了URL /main/tap 对应处理函数 app_factory和/main/boiler/shower 对应处理函数 shower.py中的app_factory。

这个案例讲的就是使用PasteDeploy模块去配置WSGI解析URL。在openstack源码中就是基于这样的模型去完成restful 的解析,处理等。当一条restful的请求如:http://192.168.252.177:5000/v2.0/token

到达服务器时,服务器处理的流程就是如上,通过配置文件查找URL处理函数,然后调用处理函数返回处理结果。

使用示例 2

paste.ini

1
2
3
4
5
6
7
8
9
10
[composite:main]
use = egg:Paste#urlmap
/blog = blog
/wiki = wiki

[app:blog]
paste.app_factory = example:Blog.factory

[app:wiki]
paste.app_factory = example:Wiki.factory

wsgi_server.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
import os
from paste import deploy
from wsgiref.simple_server import make_server

class Blog(object):
def __init__(self):
print("Init Blog.")

def __call__(self, environ, start_response):
status_code = "200 OK"
response_headers = [("Content-Type", "text/plain")]
response_body = "This is Blog's response body.".encode('utf-8')

start_response(status_code, response_headers)
return [response_body]

@classmethod
def factory(cls, global_conf, **kwargs):
print("Blog factory.")
return Blog()


class Wiki(object):
def __init__(self):
print("Init Wiki.")

def __call__(self, environ, start_response):
status_code = "200 OK"
response_headers = [("Content-Type", "text/plain")]
response_body = "This is Wiki's response body.".encode('utf-8')

start_response(status_code, response_headers)
return [response_body]

@classmethod
def factory(cls, global_conf, **kwargs):
print("Wiki factory.")
return Wiki()


if __name__ == "__main__":
app = "main"
port = 22800
conf_path = os.path.abspath('paste.ini')

# 加载 app
applications = deploy.loadapp("config:{}".format(conf_path) , app)
server = make_server("localhost", port, applications)

print('Started web server at port {}'.format(port))
server.serve_forever()

一切都准备好后,在终端执行 python wsgi_server.py来启动 web server

如果像上图一样一切正常,那么打开浏览器

注意:urlmap对url的大小写是敏感的,例如如果访问http://127.0.0.1:8000/BLOG,在url映射中未能找到大写的BLOG。

小结 2

掌握 PasteDeploy ,你只要按照以下三个步骤逐个完成即可。

1、配置 PasteDeploy使用的ini文件;

2、定义WSGI应用;

3、通过loadapp函数加载WSGI应用;



参考:

详解Paste deploy

Paste模块的世界

PasteDeploy分析

Openstack源代码分析之paste.deploy

paste deploy 解说和使用

Python.Paste指南之Deploy-概念

python Paste Deployment PasteDeploy模块介绍 中文翻译版

-------------本文结束 感谢您的阅读-------------
作者Magiceses
有问题请 留言 或者私信我的 微博
满分是10分的话,这篇文章你给几分