Edited at

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

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: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

@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と判断して、エラーメッセージを返却する


  • [pipeline:main]セクションの記述に従い、output箇所を実行する


(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アプリ機能の動作原理が理解できるようになりました。

以上です。