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

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


# ◼️ まずは、WSGIのDeployment基本動作を試してみる
## (1) サンプルアプリを動かして、基本動作を確認してみる
サンプルアプリの元ネタは、"[A WSGI Developers’ Toolkit: Paste](http://docs.repoze.org/moonshining/tools/paste.html)" と "[OpenStack API's and WSGI](http://www.slideshare.net/lhrc_mikeyp/openstack-apis-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]`セクションに関する振る舞いを定義していく。
実際のサンプルコードは、こんな感じ。

```python: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]`セクションに関する振る舞いを定義していく。
実際のサンプルコードは、こんな感じ。

```python: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の実装コード](https://github.com/openstack/heat/blob/mitaka-eol/heat/common/wsgi.py#L555-L585)を参考にして、もう少し、サンプルアプリの機能を拡張してみます。

### (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]`セクションに関する振る舞いを定義していく。
実際のサンプルコードは、こんな感じ。

```python: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]`セクションに関する振る舞いを定義していく。
実際のサンプルコードは、こんな感じ。

```python: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に応じて、振る舞いを新たに追加しました。
実際のサンプルコードは、こんな感じ。

```python: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`ファイルを準備する
サンプルアプリは、先ほどとまったく同じですが、再掲しておきます。

```python: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アプリに関する振る舞いを定義していく。
実際のサンプルコードは、こんな感じ。

```python: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記事

- [Docs: Paste Deployment](https://pastedeploy.readthedocs.io/en/latest/index.html)
- [PEP333: The WSGI Specification](http://docs.repoze.org/moonshining/pep333.html)
- [A WSGI Developers’ Toolkit: Paste](http://docs.repoze.org/moonshining/tools/paste.html)
- [OpenStack API's and WSGI](http://www.slideshare.net/lhrc_mikeyp/openstack-apis-and-wsgi)
- [How to OpenStack API and WSGI api-paste.ini Work](http://fosshelp.blogspot.com/2014/03/how-to-openstack-api-and-wsgi-api.html)
- [Getting Started with Python WSGI and Paste Deployment](http://fosshelp.blogspot.com/2014/03/getting-started-with-python-wsgi-and.html)
- [PasteDeploy で WSGI アプリケーションを設定する](http://momijiame.tumblr.com/post/26684032980/pastedeploy-で-wsgi-アプリケーションを設定する)
- [WebOb Reference] (https://docs.pylonsproject.org/projects/webob/en/stable/reference.html)

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

以上です。