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処理が実施されるようなサンプルアプリを準備していく。
[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]
セクションに関する振る舞いを定義していく。
実際のサンプルコードは、こんな感じ。
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]
セクションに関する振る舞いを定義していく。
実際のサンプルコードは、こんな感じ。
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処理が実施されるようなサンプルアプリを準備していく。
[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]
セクションに関する振る舞いを定義していく。
実際のサンプルコードは、こんな感じ。
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]
セクションに関する振る舞いを定義していく。
実際のサンプルコードは、こんな感じ。
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
の順番で開始される
[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
ファイルを準備する
さきほどと、まったく同じですが、再掲しておきます。
[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に応じて、振る舞いを新たに追加しました。
実際のサンプルコードは、こんな感じ。
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
ファイルを準備する
サンプルアプリは、先ほどとまったく同じですが、再掲しておきます。
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アプリに関する振る舞いを定義していく。
実際のサンプルコードは、こんな感じ。
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記事
- Docs: Paste Deployment
- PEP333: The WSGI Specification
- A WSGI Developers’ Toolkit: Paste
- OpenStack API's and WSGI
- How to OpenStack API and WSGI api-paste.ini Work
- Getting Started with Python WSGI and Paste Deployment
- PasteDeploy で WSGI アプリケーションを設定する
- [WebOb Reference] (https://docs.pylonsproject.org/projects/webob/en/stable/reference.html)
◼️ おわりに
サンプルアプリの動作を通じて、OpenStack内部構造で使用されているWSGIアプリ機能の動作原理が理解できるようになりました。
以上です。