LoginSignup
1
3

More than 3 years have passed since last update.

OpenStackを活用して、WSGI アプリケーションの仕組みを理解する

Last updated at Posted at 2019-02-23

OpenStackの内部機構では、WSGIアプリ機能が備わっています。
今回、「サンプルアプリを体験して、どのような挙動になるのか」を確認してみたいと思います。

◼️ まずは、WSGIのDeployment基本動作を試してみる

(1) サンプルアプリを動かして、基本動作を確認してみる

サンプルアプリの元ネタは、"A WSGI Developers’ Toolkit: Paste" と "OpenStack API's and WSGI" から引用したもの

(1-1) paste, pastedeploy, webobパッケージをインストールする

% sudo pip install paste pastedeploy webob

(1-2) configured.iniファイルを準備する

api_paste.iniファイルの[pipeline: main]セッションに記述された内容に従い、PasteDeployment処理が開始される。
すなわち、hello -> auth という順番で、PasteDeployment処理が実施されるようなサンプルアプリを準備していく。

configured.ini
[pipeline:main]
pipeline = auth hello

[app:hello]
paste.app_factory = sample_app:app_factory
name = Phred
greeting = Wilkommen

[filter:auth]
paste.filter_factory = filter_app:filter_factory

(1-3) sample_app.pyファイルを準備する

まずは、api_paste.iniファイルの[app:hello]セクションに関する振る舞いを定義していく。
実際のサンプルコードは、こんな感じ。

sample_app.py
import webob
from paste import httpserver
from paste.deploy import loadapp

class Configured(object):
    def __init__(self, name, greeting):
        self.name = name
        self.greeting = greeting

    def __call__(self, req):
       return webob.Response("Hello, %s, %s !!\n" % (self.greeting, self.name))


def app_factory(global_config, name='Johnny', greeting='Howdy'):
    return Configured(name, greeting)


if __name__ == '__main__':
    httpserver.serve(loadapp('config:configured.ini', relative_to='.'),
                     host='127.0.0.1', port='8080')

(1-3-1) Webサーバ起動時の動作

  • Webサーバが起動される
  • [app:hello]セクションのpaste.app_factoryで定義されたsample_app:app_factoryメソッドが起動される
  • Configuredクラスがオブジェクトインスタンス化される

(1-3-2) リクエスト受け付け時の動作

  • Configuredクラスの__call__メソッドが起動された場合には、httpレスポンスを返却する

(1-4) filter_app.pyファイルを準備する

つづいて、api_paste.iniファイルの[filter:auth]セクションに関する振る舞いを定義していく。
実際のサンプルコードは、こんな感じ。

filter_app.py
import webob
from webob.dec import wsgify
from webob import exc

class Middleware(object):
    def __init__(self, application):
        self.application = application

    @webob.dec.wsgify
    def __call__(self, request):
        if request.headers.get('X-Auth-Token') != 'open-sesame':
            return exc.HTTPForbidden()
        return self.application(request)

def filter_factory(global_config, **local_config):
    print("### load_config_file from [%s]"%global_config)

    def filter(app):
        print("### app=[%s]"%app)
        return Middleware(app)
    return filter

(1-4-1) Webサーバ起動時、[filter:auth]セクションに関する動作

  • [filter:auth]セクションのpaste.filter_factoryで定義されたfilter_app:auth_filter_factoryメソッドが起動される
  • その場合、クロージャfilterへの引数appには、sample_app.Configuredのオブジェクトインスタンスが渡される
  • Middlewareクラスがオブジェクトインスタンス化される
  • Middlewareクラスのコンストラクタが起動されて、self.applicationには、sample_app.Configuredのオブジェクトインスタンス値が設定される

(1-4-2) リクエスト受け付け時、[filter:auth]セクションに関する動作

  • Middlewareクラスの__call__メソッドが起動された場合には、リクエストメッセージのヘッダ情報をチェックする
  • 適切なトークンが設定されている場合には、適切なレスポンスを返却する
  • 適切なトークンが設定されていない場合には、認証NGと判断して、エラーメッセージを返却する

(1-5) サンプルアプリを起動する

サンプルアプリのWebサーバを起動すると、こんな感じの起動結果が出力される

% python sample_app.py 
### load_config_file from [{'__file__': '/root/configured.ini', 'here': '/root'}]
### app=[<sample_app.Configured object at 0x7f418808e950>]
serving on http://127.0.0.1:8080

(1-6) Webサーバにアクセスする

  • トークン情報をセットした上で、Webサーバにアクセスすると ...
% curl -H "X-Auth-Token: open-sesame" http://127.0.0.1:8080
Hello, Wilkommen, Phred !!
  • トークン情報をセットせずに、Webサーバにアクセスすると ...
% curl http://127.0.0.1:8080
<html>
 <head>
  <title>403 Forbidden</title>
 </head>
 <body>
  <h1>403 Forbidden</h1>
  Access was denied to this resource.<br /><br />



 </body>
</html>

サンプルアプリが、期待通りに動作していることが確認できた。

◼️ さらに、WSGIのDeployment拡張動作を試してみる

(2) さらに、サンプルアプリ機能を拡張してみる

heat-apiの実装コードを参考にして、もう少し、サンプルアプリの機能を拡張してみます。

(2-1) configured.iniファイルを準備する

api_paste.iniファイルの[pipeline: main]セッションに記述された内容に従い、PasteDeployment処理が開始される。
すなわち、hello -> auth -> output という順番で、PasteDeployment処理が実施されるようなサンプルアプリを準備していく。

configured.ini
[pipeline:main]
pipeline = output auth hello

[app:hello]
paste.app_factory = sample_app:app_factory
name = Phred
greeting = Wilkommen

[filter:auth]
paste.filter_factory = filter_app:auth_filter_factory

[filter:output]
paste.filter_factory = filter_app:output_filter_factory

(2-2) sample_app.pyファイルを準備する

まずは、api_paste.iniファイルの[app:hello]セクションに関する振る舞いを定義していく。
実際のサンプルコードは、こんな感じ。

sample_app.py
import webob
from webob.dec import wsgify
from paste import httpserver
from paste.deploy import loadapp


class Configured(object):
    def __init__(self, name, greeting):
        print("### Starting %s ..." % self)
        self.name = name
        self.greeting = greeting

    @webob.dec.wsgify
    def __call__(self, req):
        print("### Running %s ..."%self)
        return webob.Response("Hello, %s, %s !!\n" % (self.greeting, self.name))


def app_factory(global_config, name='Johnny', greeting='Howdy'):
    return Configured(name, greeting)


if __name__ == '__main__':
    httpserver.serve(loadapp('config:configured.ini', relative_to='.'),
                     host='127.0.0.1', port='8080')

(2-2-1) Webサーバ起動時の動作

  • Webサーバが起動される
  • [app:hello]セクションのpaste.app_factoryで定義されたsample_app:app_factoryメソッドが起動される
  • Configuredクラスがオブジェクトインスタンス化される

(2-2-2) リクエスト受け付け時の動作

  • Configuredクラスの__call__メソッドが起動された場合には、httpレスポンスを返却する

(2-3) filter_app.pyファイルを準備する

つづいて、api_paste.iniファイルの[filter:auth]セクションと、[filter:output]セクションに関する振る舞いを定義していく。
実際のサンプルコードは、こんな感じ。

filter_app.py
import webob
from webob.dec import wsgify
from webob import exc


class Middleware(object):
    def __init__(self, application):
        self.application = application

    def process_response(self, response):
        return response

    @webob.dec.wsgify
    def __call__(self, req):
        response = self.process_request(req)
        if response:
            return response
        response = req.get_response(self.application)
        return self.process_response(response)


class AuthFilter(Middleware):
    def __init__(self, application):
        print("### Starting %s ..." % self)
        super(AuthFilter, self).__init__(application)

    def process_request(self, request):
        print("### Running %s ..." % self)
        if request.headers.get('X-Auth-Token') != 'open-sesame':
            return exc.HTTPForbidden()
        return request.get_response(self.application)


class OutputToken(Middleware):
    def __init__(self, application):
        print("### Starting %s ..." % self)
        super(OutputToken, self).__init__(application)

    def process_request(self, request):
        print("### Running %s ..."%self)
        x_auth_token = request.headers.get('X-Auth-Token')
        print("### Receiving Request: [X-Auth-Token]=\"%s\"" % x_auth_token)
        return None


def auth_filter_factory(global_config, **local_config):
    print("### load_config_file from [%s]" % global_config)

    def filter(app):
        print("### Starting auth_filter_factory: app=[%s]" % app)
        return AuthFilter(app)
    return filter


def output_filter_factory(global_config, **local_config):
    def filter(app):
        print("### Starting output_filter_factory: app=[%s]" % app)
        return OutputToken(app)
    return filter

(2-3-1) Webサーバ起動時、[filter:auth]セクションに関する動作

  • [filter:auth]セクションのpaste.filter_factoryで定義されたfilter_app:auth_filter_factoryメソッドが起動される
  • その場合、クロージャfilterへの引数appには、sample_app.Configuredのオブジェクトインスタンスが渡される
  • Middlewareクラスがオブジェクトインスタンス化される
  • Middlewareクラスのコンストラクタが起動されて、self.applicationには、sample_app.Configuredのオブジェクトインスタンス値が設定される

(2-3-2) Webサーバ起動時、[filter:output]セクションに関する動作

  • [filter:output]セクションのpaste.filter_factoryで定義されたfilter_app:output_filter_factoryメソッドが起動される
  • その場合、クロージャfilterへの引数appには、filter_app.AuthFilterのオブジェクトインスタンスが渡される
  • Middlewareクラスがオブジェクトインスタンス化される
  • Middlewareクラスのコンストラクタが起動されて、self.applicationには、filter_app.AuthFilterのオブジェクトインスタンス値が設定される

(2-3-3) リクエスト受け付け時、[filter:auth]セクションに関する動作

  • Middlewareクラスの__call__メソッドが起動された場合には、リクエストメッセージのヘッダ情報をチェックする
  • 適切なトークンが設定されている場合には、適切なレスポンスを返却する
  • 適切なトークンが設定されていない場合には、認証NGと判断して、エラーメッセージを返却する
  • [pipeline:main]セクションの記述に従い、output箇所を実行する

(2-3-4) リクエスト受け付け時、[filter:output]セクションに関する動作

  • Middlewareクラスの__call__メソッドが起動された場合には、リクエストメッセージのヘッダ情報X-Auth-Tokenの内容を画面出力する

(2-4)サンプルアプリを起動する

サンプルアプリのWebサーバを起動すると、こんな感じの起動結果が出力される

% python sample_app.py 
### Starting <sample_app.Configured object at 0x7fc7bce2d4d0> ...
### load_config_file from [{'__file__': '/root/configured.ini', 'here': '/root'}]
### Starting auth_filter_factory: app=[<sample_app.Configured object at 0x7fc7bce2d4d0>]
### Starting <filter_app.AuthFilter object at 0x7fc7bcde5e10> ...
### Starting output_filter_factory: app=[<filter_app.AuthFilter object at 0x7fc7bcde5e10>]
### Starting <filter_app.OutputToken object at 0x7fc7bcde5350> ...
serving on http://127.0.0.1:8080

(2-5) Webサーバにアクセスする

  • トークン情報をセットした上で、Webサーバにアクセスすると ...
% curl -H "X-Auth-Token: open-sesame" http://127.0.0.1:8080
Hello, Wilkommen, Phred !!
  • Webサーバを起動したターミナルを、再度、確認すると、さきほど、Webサーバにアクセスした際に、ヘッダ情報に設定した"X-Auth-Token"が画面出力されていますね
% python sample_app.py 
### Starting <sample_app.Configured object at 0x7fc7bce2d4d0> ...
### load_config_file from [{'__file__': '/root/configured.ini', 'here': '/root'}]
### Starting auth_filter_factory: app=[<sample_app.Configured object at 0x7fc7bce2d4d0>]
### Starting <filter_app.AuthFilter object at 0x7fc7bcde5e10> ...
### Starting output_filter_factory: app=[<filter_app.AuthFilter object at 0x7fc7bcde5e10>]
### Starting <filter_app.OutputToken object at 0x7fc7bcde5350> ...
serving on http://127.0.0.1:8080
### Running <filter_app.OutputToken object at 0x7fc7bcde5350> ...
### Receiving Request: [X-Auth-Token]="open-sesame"
### Running <filter_app.AuthFilter object at 0x7fc7bcde5e10> ...
### Running <sample_app.Configured object at 0x7fc7bce2d4d0> ...

(2-6) サンプルアプリ拡張を動作させて、わかったこと

以上で、サンプルアプリ拡張を通じて、PasteDeployment処理の基本/拡張動作が確認できました。

  • Webサーバ起動時のpipeline処理では、hello -> auth -> outputの順番で開始される
  • リクエスト受け付け時のpipelineには、output -> auth -> helloの順番で開始される
configured.ini
[pipeline:main]
pipeline = output auth hello

[app:hello]
paste.app_factory = sample_app:app_factory
name = Phred
greeting = Wilkommen

[filter:auth]
paste.filter_factory = filter_app:auth_filter_factory

[filter:output]
paste.filter_factory = filter_app:output_filter_factory

◼️ さらに、クライアントからのURLに応じたWSGIアプリを拡張してみる

(3) さらに、routesを活用して、WSGIアプリ機能を拡張してみる

クライアントからのURLに応じて、WSGIアプリの振る舞いを制御できるよう、サンプルアプリを拡張していきます。

(3-1) routesパッケージをインストールする

% sudo pip install routes

(3-2) configured.iniファイルを準備する

さきほどと、まったく同じですが、再掲しておきます。

configured.ini
[pipeline:main]
pipeline = output auth hello

[app:hello]
paste.app_factory = sample_app:app_factory
name = Phred
greeting = Wilkommen

[filter:auth]
paste.filter_factory = filter_app:auth_filter_factory

[filter:output]
paste.filter_factory = filter_app:output_filter_factory

(3-3) sample_app.pyファイルを準備する

URLに応じて、振る舞いを新たに追加しました。
実際のサンプルコードは、こんな感じ。

sample_app.py
import webob
import routes
import routes.middleware
from wsgi import Resource, Router
from webob.dec import wsgify
from webob.exc import HTTPNotFound
from paste import httpserver
from paste.deploy import loadapp


class Configured(Router):
    def __init__(self, name, greeting):
        mapper = routes.Mapper()

        stacks_resource = Resource(Controller(name, greeting))
        with mapper.submapper(controller=stacks_resource,
                              path_prefix="/greeting") as stack_mapper:
            stack_mapper.connect("stack_index",
                                 "/hello",
                                 action="hello",
                                 conditions={'method': 'GET'})

            stack_mapper.connect("stack_index",
                                 "/goodbye",
                                 action="goodbye",
                                 conditions={'method': 'GET'})

        super(Configured, self).__init__(mapper)



class Controller(object):
    def __init__(self, name, greeting):
        self.name = name
        self.greeting = greeting

    def hello(self, req):
        return "Hello, %s, %s !!\n" % (self.greeting, self.name)

    def goodbye(self, req):
        return "Good Bye, %s, %s !!\n" % (self.greeting, self.name)


def app_factory(global_config, name='Johnny', greeting='Howdy'):
    return Configured(name, greeting)


if __name__ == '__main__':
    httpserver.serve(loadapp('config:configured.ini', relative_to='.'),
                     host='127.0.0.1', port='8080')

(3-4) filter_app.pyファイルを準備する

サンプルアプリは、先ほどとまったく同じですが、再掲しておきます。

filter_app.py
import webob
from webob.dec import wsgify
from webob import exc


class Middleware(object):
    def __init__(self, application):
        self.application = application

    def process_response(self, response):
        return response

    @webob.dec.wsgify
    def __call__(self, req):
        response = self.process_request(req)
        if response:
            return response
        response = req.get_response(self.application)
        return self.process_response(response)


class AuthFilter(Middleware):
    def __init__(self, application):
        super(AuthFilter, self).__init__(application)

    def process_request(self, request):
        if request.headers.get('X-Auth-Token') != 'open-sesame':
            return exc.HTTPForbidden()
        return request.get_response(self.application)


class OutputToken(Middleware):
    def __init__(self, application):
        super(OutputToken, self).__init__(application)

    def process_request(self, request):
        x_auth_token = request.headers.get('X-Auth-Token')
        print("Receiving Request: [X-Auth-Token]=\"%s\"" % x_auth_token)
        return None


def auth_filter_factory(global_config, **local_config):

    def filter(app):
        return AuthFilter(app)
    return filter


def output_filter_factory(global_config, **local_config):
    def filter(app):
        return OutputToken(app)
    return filter

(3-5) wsgi.pyファイルを準備する

つづいて、WSGIアプリに関する振る舞いを定義していく。
実際のサンプルコードは、こんな感じ。

wsgi.py
import webob
import routes
import routes.middleware
from webob.dec import wsgify
from webob.exc import HTTPNotFound
from paste import httpserver
from paste.deploy import loadapp


class Router(object):
    def __init__(self, mapper):
        self.map = mapper
        self._router = routes.middleware.RoutesMiddleware(self._dispatch, self.map)

    @webob.dec.wsgify
    def __call__(self, req):
        return self._router

    @webob.dec.wsgify
    def _dispatch(self, req):
        match = req.environ['wsgiorg.routing_args'][1]
        if not match:
            return webob.exc.HTTPNotFound()
        app = match['controller']
        return app


class Resource(object):
    def __init__(self, controller):
        self.controller = controller

    @webob.dec.wsgify
    def __call__(self, request):
        action = self.get_action(request.environ)
        action_result = self.dispatch(self.controller, action, request)
        return webob.Response(action_result)

    def dispatch(self, obj, action, *args, **kwargs):
        method = getattr(obj, action)
        return method(*args, **kwargs)

    def get_action(self, request_environment):
        return request_environment['wsgiorg.routing_args'][1]['action']

(3-6)サンプルアプリを起動する

サンプルアプリのWebサーバを起動すると、こんな感じの起動結果が出力される

% python sample_app.py
serving on http://127.0.0.1:8080

(3-7) Webサーバにアクセスする

(3-7-1) クライアントより、helloメソッドを起動する

  • トークン情報をセットした上で、Webサーバにアクセスすると ...
% curl -H "X-Auth-Token: open-sesame" http://127.0.0.1:8080/greeting/hello
Hello, Wilkommen, Phred !!

(3-7-2) クライアントより、goodbyメソッドを起動する

  • トークン情報をセットした上で、Webサーバにアクセスすると ...
% curl -H "X-Auth-Token: open-sesame" http://127.0.0.1:8080/greeting/goodbye
Good Bye, Wilkommen, Phred !!

はい、期待通りに動作できました。

◼️ 参考になった Web/Blog記事

◼️ おわりに

サンプルアプリの動作を通じて、OpenStack内部構造で使用されているWSGIアプリ機能の動作原理が理解できるようになりました。

以上です。

1
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
3