美文网首页
OpenStack Restful框架

OpenStack Restful框架

作者: 圣地亚哥_SVIP | 来源:发表于2019-04-30 15:15 被阅读0次

    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

    相关文章

      网友评论

          本文标题:OpenStack Restful框架

          本文链接:https://www.haomeiwen.com/subject/kbrvnqtx.html