OpenStack Restful框架

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_filterget_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启动脚本。

GITHUB链接

测试方法:
# virtualenv venv
# source venv/bin/activate
# python3.7 setup.py install --install-scripts ./venv/bin/
# ./venv/bin/crane_cli_ai

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,530评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,403评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,120评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,770评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,758评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,649评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,021评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,675评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,931评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,751评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,410评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,004评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,969评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,042评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,493评论 2 343