openstack基于Paste+Pastedeploy+route+webob框架开发,主要使用到的一些模块是:
- eventlet: python 的高并发网络库
- paste.deploy: 用于发现和配置 WSGI application 和 server 的库
- routes: 处理 http url mapping 的库
- webob: 处理HTTP请求并提供了一个对象来方便的处理返回response消息
上述框架实现起来比较复杂,目前新的版本中,都利用pecan的框架。pecan的框架相较于上述框架,更加简单。telemetry项目主要使用的pecan框架,如gnocchi采用的是paste.deploy+pecan实现。
PasteDeploy
PasteDeploy用来发现和配置WSGI应用的一套系统。WSGI的使用者,提供了一个单一简单的函数(loadapp)用于通过配置文件或python egg中加载WSGI。对于WSGI提供者,仅仅要求提供一个单一的简单的应用入口。
无需应用者展示应用的具体实现。
PasteDeploy配置文件
PasteDeploy定义了以下几个部件:
- app:callable object,WSGI服务
- filter: 过滤器,主要用于预处理的一些工作,如身份验证等。执行完毕之后直接返回或是交给下一个filter/app继续处理。filter是一个callable object,参数是app,对app进行封装后返回,
- pipeline:由若干个filter和1个APP服务
- composite:实现对不同app的分发。如根据url的参数分发给不同的app。
如下示例:
[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
use = egg:Paste#urlmap
/ = home
/blog = blog
/wiki = wiki
/cms = config:cms.ini
composite:main
: 表示分发请求至不同的应用;
use = egg:Paste#urlmap
: 表示使用Paste Package中的urlmap应用。urlmap主要用于根据路径前缀将请求映射至不同的应用;
/cms = config:cms.ini
: 引用相同目录下的另一个配置文件:cms.ini;
其余表示不同的前缀对应至不同的应用,在pastedeploy中,每一个WSGI应用都需要有一个自己的[section]。
[app:home]
use = egg:Paste#static
document_root = %(here)s/htdocs
use = egg:Paste#static
: 表示使用Paste Package中的static应用,在此处表示静态文件。document_root配置项, %(var_name)s表示变量替换,变量从全局配置[DEFAULT]中提取。此处%(here)s表示当前配置文件所在目录。
[filter-app:blog]
use = egg:Authentication#auth
next = blogapp
roles = admin
htpasswd = /home/me/users.htpasswd
[filter-app:blog]
:表示对一个app使用过滤器;
use = egg:Authentication#auth
: 使用Authentication Package中auth模块;
next = blogapp
: 表示处理后的app转移至blogapp处理
roles,htpasswd
: auth处理的参数
[app:blogapp]
use = egg:BlogApp
database = sqlite:/home/me/blog.db
[app:wiki]
use = call:mywiki.main:application
database = sqlite:/home/me/wiki.db
表示两个WSGI应用,blogapp和wiki。使用app的方式上有所区别,一个是利用egg的入口;一个指明某个模块中的可调用的app
配置说明
pastedeploy主要用于加载WSGI应用。主要python框架支持WSGI,就可以使用pastedeploy。如给定一个URI,用法如下:
from paste.deploy import loadapp
wsgi_app = loadapp('config:/path/to/config.ini')
URI格式如下:
- config:
- egg:
config:URIs
"config:URIs"利用config指向配置文件。关键字参数relative_to
相对目录索引配置文件。relative_to
非相对于工作目录,如果有config:URI在另一个配置文件中,则是相对此位置。
配置文件格式:INI格式,大小写敏感。
Application: 可以一个文件中,定义多个App,每一个APP都有一个单独的Section。APP Section以 app: 开头。
两种方式引用Application:
- 引用另一个URI或name
[app:myapp]
use = config:another_config_file.ini#app_name
# or any URI:
[app:myotherapp]
use = egg:MyApp
# or a callable from a module:
[app:mythirdapp]
use = call:my.project:myapplication
# or even another section:
[app:mylastapp]
use = myotherapp
- 定义一个App,指向对应的Python Code
[app:myapp]
paste.app_factory = myapp.modulename:app_factory
配置项
配置分为global config及local config。
global config:位于[DEFAULT]section下,所有section共享的
local config: 位于具体section下,独有。同时,可在本地section覆盖global config,如下:
[DEFAULT]
admin_email = webmaster@example.com
[app:main]
use = ...
set admin_email = bob@example.com
定义工厂函数
以下几类工厂函数:
- paste.app_factory,
- paste.composite_factory
- paste.filter_factory
- paste.server_factory
paste.app_factory,最常用的工厂函数,需要使用者定义如下:
def app_factory(global_config, **local_conf):
return wsgi_app
其中:global_config是包含全局变量的字典,local_conf:局部变量;返回一个wsgi app。
paste.composite_factory:
def composite_factory(loader, global_config, **local_conf):
return wsgi_app
其中:
loader: 包含多个方法:get_app(name_or_uri, global_conf=None)
,根据name返回一个app;get_filter
和get_server
;
示例:
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
上述定义了一个工厂函数。
配置文件如下:
[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
pipeline_factory_uri: 指向此工厂函数。
paste.filter_factory: 返回filter的工厂函数
示例:
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']
配置文件示例:
[filter:auth]
use = <filter_factory>
paste.server_factory
返回一个serve,此serve接收app参数。
示例:
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
pecan简介
Pecan 框架的目标是实现一个采用对象分发方式进行 URL 路由的轻量级 Web 框架。生成app的接口:
app = pecan.make_app(root,**kw)
make_app返回一个Pecan object(wsgi object)。此函数通常是在项目的app.py文件中由setup_app调用。
Pecan(*args, **kw)
包含以下参数:
- root: 字符串,表示一个root controller
- default_renderer: 模板渲染引擎,默认是mako
- template_path: 模板所在路径,相对位置路径
- hooks: pecan.hooks.PecanHook Object,用于处理request
- custom_renderers: 自定义模板引擎
- extra_template_vars:额外的模板参数
- force_canonical:布尔值,项目是否要求标准URL
- guess_content_type_from_ext: 是否从URL的扩展猜测返回内容的content type
- use_context_locals
- request_cls
- response_cls
通过loaddeploy加载对应的app,以gnocchi为例,api-paste.ini文件:
[app:gnocchiv1]
paste.app_factory = gnocchi.rest.app:app_factory
root = gnocchi.rest.api.V1Controller
app_factory
示例如下:
def _setup_app(root, conf, not_implemented_middleware):
app = pecan.make_app(
root,
hooks=(GnocchiHook(conf),),
guess_content_type_from_ext=False,
custom_renderers={"json": JsonRenderer}
)
return app
def app_factory(global_config, **local_conf):
global APPCONFIGS
appconfig = APPCONFIGS.get(global_config.get('configkey'))
return _setup_app(root=local_conf.get('root'), **appconfig)
其中:
local_conf.get('root'): gnocchi.rest.api.V1Controller
入口函数调用如下内容,生成一个WSGI的app:
app = deploy.loadapp("config:" + cfg_path, name=appname,
global_conf={'configkey': configkey})
cfg_path: api-paste.ini文件位置
name: 加载对应的app,此处为gnocchiv1
上述代码利用app_factory
生成一个Pecan Object(WSGI)。
pecan.hooks
主要是在处理request的不同阶段生成对应的hook。
主要方法:
after(state)
:
重写此方法,在请求被controller处理后调用;
before(state)
:
重写此方法,在请求被controller处理前调用;
on_error(state, e)
:
重写此方法,在处理请求,异常发生时调用;
on_route
:
重写此方法,在请求开始routing时,调用;
参考gnocchi中的使用:
class GnocchiHook(pecan.hooks.PecanHook):
def __init__(self, conf):
self.backends = {}
self.conf = conf
def on_route(self, state):
state.request.storage = self._lazy_load('storage')
state.request.conf = self.conf
@staticmethod
def after(state):
state.request.body_file.read()
def _lazy_load(self, name):
# NOTE(sileht): We don't care about raise error here, if something
# fail, this will just raise a 500, until the backend is ready.
if name not in self.backends:
with self.BACKEND_LOCKS[name]:
# Recheck, maybe it have been created in the meantime.
if name not in self.backends:
if name == "storage":
self.backends[name] = (
gnocchi_storage.get_driver(self.conf)
)
else:
raise RuntimeError("Unknown driver %s" % name)
return self.backends[name]
自定义了on_route
,after
,方法。
root
Root controller,在api-paste.ini中:
root = gnocchi.rest.api.V1Controller
V1Controller的定义:
class V1Controller(object):
def __init__(self):
# FIXME(sileht): split controllers to avoid lazy loading
from gnocchi.rest.aggregates import api as agg_api
from gnocchi.rest import influxdb
self.sub_controllers = {
"search": SearchController(),
"archive_policy": ArchivePoliciesController(),
"archive_policy_rule": ArchivePolicyRulesController(),
"metric": MetricsController(),
"batch": BatchController(),
"resource": ResourcesByTypeController(),
"resource_type": ResourceTypesController(),
"aggregation": AggregationController(),
"capabilities": CapabilityController(),
"status": StatusController(),
"aggregates": agg_api.AggregatesController(),
"influxdb": influxdb.InfluxDBController(),
}
for name, ctrl in self.sub_controllers.items():
setattr(self, name, ctrl)
if PROMETHEUS_SUPPORTED:
setattr(self, "prometheus", PrometheusController())
@pecan.expose('json')
def index(self):
return {
"version": "1.0",
"links": [
{"rel": "self",
"href": pecan.request.application_url}
] + [
{"rel": name,
"href": pecan.request.application_url + "/" + name}
for name in sorted(self.sub_controllers)
]
}
V1Controller是请求的入口,根据url路径,转发给对应的SubController进行处理。
也可以通过_lookup(self,para,*remainder)
转发给SubController。如下:
@pecan.expose()
def _lookup(self, id, *remainder):
return ResourceController(self._resource_type, id), remainder
RestController
利用RestController提供restful routing。RestController提供的routes如下:
- get_one: Display one record. GET /books/1
- get_all: Display all records in a resource. GET /books/
- get: A combo of get_one and get_all. GET /books/ and GET /books/1
- new: Display a page to create a new resource. GET /books/new
- edit: Display a page to edit an existing resource. GET /books/1/edit
- post: Create a new record. POST /books/
- put: Update an existing record. POST /books/1?_method=put and PUT /books/1
- get_delete: Display a delete confirmation page. GET /books/1/delete
- delete: Delete an existing record. POST /books/1?_method=delete and DELETE /books/1
基于oslo.log oslo.config pastedeploy pecan完成一个示例:
api-paste.ini文件内容如下:
[composite:crane+basic]
use = egg:Paste#urlmap
/v1 = cranev1+noauth
[pipeline:cranev1+noauth]
pipeline = cranev1
[app:cranev1]
paste.app_factory = crane.app:app_factory
root = crane.root.V1Controller
查看app.py文件内容:
def load_app(conf):
global APPCONFIGS
# Build the WSGI app
cfg_path = conf.api.paste_config
if not os.path.isabs(cfg_path):
cfg_path = conf.find_file(cfg_path)
if cfg_path is None or not os.path.exists(cfg_path):
LOG.debug("No api-paste configuration file found! Using default.")
cfg_path = os.path.abspath(pkg_resources.resource_filename(
__name__, "api-paste.ini"))
config = dict(conf=conf)
configkey = str(uuid.uuid4())
APPCONFIGS[configkey] = config
LOG.info("WSGI config used: %s", cfg_path)
appname = "crane+basic"
return deploy.loadapp("config:" + cfg_path, name=appname,
global_conf={'configkey': configkey})
def _setup_app(root,conf):
app = pecan.make_app(
root,
guess_content_type_from_ext=False
)
return app
def app_factory(global_config, **local_conf):
global APPCONFIGS
appconfig = APPCONFIGS.get(global_config.get('configkey'))
return _setup_app(root=local_conf.get('root'), **appconfig)
注释:
- load_app: 加载并解析api-paste.ini文件,加载[composite:crane+basic]
- _setup_app: 加载pecan wsgi application
- app_factory: api-paste.ini中,加载此[app:cranev1],在调用_setup_app,根据参数root,调用V1Controller
api.py文件内容解析:
def build_wsgi_app(argv=None):
return app.load_app(service.prepare_service())
def api():
# Compat with previous pbr script
try:
double_dash = sys.argv.index("--")
except ValueError:
double_dash = None
else:
sys.argv.pop(double_dash)
conf = cfg.ConfigOpts()
'''for opt in opts.api_opts:
cp = copy.copy(opt)
cp.default = None
conf.register_cli_opt(cp)
'''
conf = service.prepare_service(conf)
if double_dash is not None:
# NOTE(jd) Wait to this stage to log so we're sure the logging system
# is in place
LOG.warning(
"No need to pass `--' in crane-api command line anymore, "
"please remove")
uwsgi = spawn.find_executable("uwsgi")
if not uwsgi:
LOG.error("Unable to find `uwsgi'.\n"
"Be sure it is installed and in $PATH.")
return 1
workers = utils.get_default_workers()
args = [
"--if-not-plugin", "python", "--plugin", "python", "--endif",
"--%s" % conf.api.uwsgi_mode, "%s:%d" % (
conf.api.host,
conf.api.bind_port),
"--master",
"--enable-threads",
"--thunder-lock",
"--hook-master-start", "unix_signal:15 gracefully_kill_them_all",
"--die-on-term",
"--processes", str(math.floor(workers * 1.5)),
"--threads", str(workers),
"--lazy-apps",
"--chdir", "/",
"--wsgi", "crane.wsgi",
"--pyargv", " ".join(sys.argv[1:]),
]
if conf.api.uwsgi_mode == "http":
args.extend([
"--so-keepalive",
"--http-keepalive",
"--add-header", "Connection: Keep-Alive"
])
virtual_env = os.getenv("VIRTUAL_ENV")
if virtual_env is not None:
args.extend(["-H", os.getenv("VIRTUAL_ENV", ".")])
return os.execl(uwsgi, uwsgi, *args)
主要是生成wsgi启动脚本。
测试方法:
# virtualenv venv
# source venv/bin/activate
# python3.7 setup.py install --install-scripts ./venv/bin/
# ./venv/bin/crane_cli_ai