(Stein版) OpenStack heat内部構造を探ってみる[heat-api編]

NFVオーケストレーションを実現する手段として、一般的に、OpenStack heatが活用されることが多いと思います。
そこで、今回、heat-api内部構造を調査することにより、その動作メカニズムを理解していきたいと思います。

# ◼️ そもそも、OpenStack heatって、どんなもの？
Qiita記事「[Mitaka版 OpenStack Heat環境をセットアップしてみた](https://qiita.com/ttsubo/items/7439116ab0e0484ef654)」が参考になると思います。

# ◼️ OpenStack heatでの、大まかな処理の流れ
OpenStack heatは、リクエストに応じて、適切なクラウドオペレーション（novaインスタンス作成など）を行います。

- heat-apiが、ユーザからのリクエストを受け付ける。
- heat-api内部で、必要なコンテキスト処理（token情報の入手等）を実施する。
- heat-api内部で、OpenStack固有の権限設定`policy.json` の記述に従って、実行可否を判定する。
- RabbitMQ等のメッセージング機構を介して、heat-engine側で適切なオーケストレーション処理を実施します。

なお、メッセージング処理機構は、Qiita記事「[OpenStackメッセージング機構を探ってみる]
(https://qiita.com/ttsubo/items/1f2884d41c3fe3129c5e)」を参照ください。

![Heat Architecture.002.png](https://qiita-image-store.s3.amazonaws.com/0/103595/1c1dda05-93ca-9ed5-4a78-34007c78077d.png)
## (1) heat-api起動の流れは、どうなっているのか？
こちらのスライド"[OpenStack API's and WSGI](http://www.slideshare.net/lhrc_mikeyp/openstack-apis-and-wsgi)"からの引用です。
![名称未設定.001.png](https://qiita-image-store.s3.amazonaws.com/0/103595/534f386c-2294-87b3-b144-f7adeef82ffb.png)
今回、heat-api動作の流れを調査する上で、これらのキーワードを意識しておくことが大切になりそうです。

## (2) heat向けのリクエストを、どうやって処理しているのか？
heat-api内部にて、受け付けたリクエストメッセージを適切に処理する場合、WSGI アプリケーションの機構が関与しているようです。そこで、まずは、WSGI アプリケーションの仕組みを理解する必要があります。
過去のQiita記事「[OpenStackを活用して、WSGI アプリケーションの仕組みを理解する](https://qiita.com/ttsubo/items/0a1abecce272559edc94)を参照ください。

# ◼️ OpenStack heat環境として、`/etc/heat/api_paste.ini`ファイルを事前確認しておく
`/etc/heat/api_paste.ini` が配備されているので、事前に内容を確認しておく

```python:api_paste.ini
# heat-api pipeline
[pipeline:heat-api]
pipeline = cors request_id faultwrap http_proxy_to_wsgi versionnegotiation authurl authtoken context osprofiler apiv1app

... (snip)

[app:apiv1app]
paste.app_factory = heat.common.wsgi:app_factory
heat.app_factory = heat.api.openstack.v1:API

... (snip)

[filter:versionnegotiation]
paste.filter_factory = heat.common.wsgi:filter_factory
heat.filter_factory = heat.api.openstack:version_negotiation_filter

[filter:cors]
paste.filter_factory = oslo_middleware.cors:filter_factory
oslo_config_project = heat

[filter:faultwrap]
paste.filter_factory = heat.common.wsgi:filter_factory
heat.filter_factory = heat.api.openstack:faultwrap_filter

... (snip)

[filter:context]
paste.filter_factory = heat.common.context:ContextMiddleware_filter_factory

... (snip)

[filter:http_proxy_to_wsgi]
paste.filter_factory = oslo_middleware:HTTPProxyToWSGI.factory

# Middleware to set auth_url header appropriately
[filter:authurl]
paste.filter_factory = heat.common.auth_url:filter_factory

# Auth middleware that validates token against keystone
[filter:authtoken]
paste.filter_factory = keystonemiddleware.auth_token:filter_factory

... (snip)

# Middleware to set x-openstack-request-id in http response header
[filter:request_id]
paste.filter_factory = oslo_middleware.request_id:RequestId.factory

[filter:osprofiler]
paste.filter_factory = osprofiler.web:WsgiMiddleware.factory
```

# ◼️ heat-api起動処理の動作メカニズムを調査してみる
まず、["heat/cmd/api.py"](https://github.com/openstack/heat/blob/stable/stein/heat/cmd/api.py)のPythonスクリプト動作の流れを、確認していきます。

## (1) [load_paste_app](https://github.com/openstack/heat/blob/stable/stein/heat/cmd/api.py#L52)メソッドが起動される

`load_paste_app`メソッドは、こんな感じ...

```python:heat/common/config.py
def load_paste_app(app_name=None):
    """Builds and returns a WSGI app from a paste config file.

    We assume the last config file specified in the supplied ConfigOpts
    object is the paste config file.

    :param app_name: name of the application to load

    :raises RuntimeError: when config file cannot be located or application
            cannot be loaded from config file
    """
    if app_name is None:
        app_name = cfg.CONF.prog

    # append the deployment flavor to the application name,
    # in order to identify the appropriate paste pipeline
    app_name += _get_deployment_flavor()

    conf_file = _get_deployment_config_file()
    if conf_file is None:
        raise RuntimeError(_("Unable to locate config file [%s]") %
                           cfg.CONF.paste_deploy['api_paste_config'])

    try:
        app = wsgi.paste_deploy_app(conf_file, app_name, cfg.CONF)

        # Log the options used when starting if we're in debug mode...
        if cfg.CONF.debug:
            cfg.CONF.log_opt_values(logging.getLogger(app_name),
                                    logging.DEBUG)

        return app
    except (LookupError, ImportError) as e:
        raise RuntimeError(_("Unable to load %(app_name)s from "
                             "configuration file %(conf_file)s."
                             "\nGot: %(e)r") % {'app_name': app_name,
                                                'conf_file': conf_file,
                                                'e': e})
``` 

そして、`app_name`には、初期値として["heat-api"](https://github.com/openstack/heat/blob/stable/stein/heat/cmd/api.py#L45)が設定される

### (1-1) _get_deployment_flavorメソッドが起動される
`/etc/heat/heat.conf`ファイルに、flavorパラメータを設定することが可能だが、["Heat Configuration Sample"](https://docs.openstack.org/heat/stein/configuration/sample_config.html)によると、デフォルトでは、設定されていないので、特に、何もしない。

```
[paste_deploy]

#
# From heat.common.config
#

# The flavor to use. (string value)
#flavor = <None>

# The API paste config file to use. (string value)
#api_paste_config = api-paste.ini
```
### (1-2) _get_deployment_config_fileメソッドが起動される
`conf_file`には、["/etc/heat/api-paste.ini"](https://github.com/openstack/heat/blob/stable/stein/etc/heat/api-paste.ini)ファイルの保管先としてパス情報が設定される。


## (2) [paste_deploy_app](https://github.com/openstack/heat/blob/stable/stein/heat/common/config.py#L498)メソッドが起動される

`paste_deploy_app`メソッドは、こんな感じ...

```python:heat/common/wsgi.py
def paste_deploy_app(paste_config_file, app_name, conf):
    """Load a WSGI app from a PasteDeploy configuration.

    Use deploy.loadapp() to load the app from the PasteDeploy configuration,
    ensuring that the supplied ConfigOpts object is passed to the app and
    filter constructors.

    :param paste_config_file: a PasteDeploy config file
    :param app_name: the name of the app/pipeline to load from the file
    :param conf: a ConfigOpts object to supply to the app and its filters
    :returns: the WSGI app
    """
    setup_paste_factories(conf)
    try:
        return loadwsgi.loadapp("config:%s" % paste_config_file, name=app_name)
    finally:
        teardown_paste_factories()
```

### (2-1) [setup_paste_factoriesメソッド](https://github.com/openstack/heat/blob/stable/stein/heat/common/wsgi.py#L1135)が起動される

`setup_paste_factories`メソッドは、こんな感じ...

```python:heat/common/wsgi.py
def setup_paste_factories(conf):
    """Set up the generic paste app and filter factories.

    Set things up so that:

      paste.app_factory = heat.common.wsgi:app_factory

    and

      paste.filter_factory = heat.common.wsgi:filter_factory

    work correctly while loading PasteDeploy configuration.

    The app factories are constructed at runtime to allow us to pass a
    ConfigOpts object to the WSGI classes.

    :param conf: a ConfigOpts object
    """
    global app_factory, filter_factory
    app_factory = AppFactory(conf)
    filter_factory = FilterFactory(conf)
```

ここでは、`AppFactory`, `FilterFactory`クラスのアクティベート処理が行われる


#### (2-1-1) [AppFactoryクラス](https://github.com/openstack/heat/blob/stable/stein/heat/common/wsgi.py#L1112)が呼び出される

`AppFactory`クラスは、こんな感じ...

```python:heat/common/wsgi.py
class AppFactory(BasePasteFactory):
    """A Generic paste.deploy app factory.

    This requires heat.app_factory to be set to a callable which returns a
    WSGI app when invoked. The format of the name is <module>:<callable> e.g.

      [app:apiv1app]
      paste.app_factory = heat.common.wsgi:app_factory
      heat.app_factory = heat.api.cfn.v1:API

    The WSGI app constructor must accept a ConfigOpts object and a local config
    dict as its two arguments.
    """

    KEY = 'heat.app_factory'

    def __call__(self, global_conf, **local_conf):
        """The actual paste.app_factory protocol method."""
        factory = self._import_factory(local_conf)
        return factory(self.conf, **local_conf)
```

グローバル変数`app_factory`に保存される。
この段階で、`[app:apiv1app]`セッションでの、`paste.app_factory = heat.common.wsgi:app_factory`に基づく、`app_factory`メソッドの起動準備が完了することになる。

```/etc/heat/api_paste.ini

... (snip)

[app:apiv1app]
paste.app_factory = heat.common.wsgi:app_factory
heat.app_factory = heat.api.openstack.v1:API
```
この後の`deploy.loadapp`処理の開始に伴い、`AppFactory`インスタンスの`__call__`メソッドが起動された場合には、_import_factoryメソッドが呼ばれる

なお、`local_conf`には、`{'heat.app_factory': 'heat.api.openstack.v1:API'}`という情報が設定されている

#### (2-1-2) [FilterFactoryクラス](https://github.com/openstack/heat/blob/stable/stein/heat/common/wsgi.py#L1113)が呼び出される

`FilterFactory`クラスは、こんな感じ...

```python:heat/common/wsgi.py
class FilterFactory(AppFactory):
    """A Generic paste.deploy filter factory.

    This requires heat.filter_factory to be set to a callable which returns a
    WSGI filter when invoked. The format is <module>:<callable> e.g.

      [filter:cache]
      paste.filter_factory = heat.common.wsgi:filter_factory
      heat.filter_factory = heat.api.middleware.cache:CacheFilter

    The WSGI filter constructor must accept a WSGI app, a ConfigOpts object and
    a local config dict as its three arguments.
    """

    KEY = 'heat.filter_factory'

    def __call__(self, global_conf, **local_conf):
        """The actual paste.filter_factory protocol method."""
        factory = self._import_factory(local_conf)

        def filter(app):
            return factory(app, self.conf, **local_conf)

        return filter
```

グローバル変数`filter_factory`に保存される。
この段階で、`[filter:versionnegotiation]`セッション, `[filter:faultwrap]`セッションでの、`paste.filter_factory = heat.common.wsgi:filter_factory`に基づく、`filter_factory`メソッドの起動準備が完了することになる。

```/etc/heat/api_paste.ini

# heat-api pipeline
[pipeline:heat-api]
pipeline = cors request_id faultwrap http_proxy_to_wsgi versionnegotiation authurl authtoken context osprofiler apiv1app

... (snip)

[filter:versionnegotiation]
paste.filter_factory = heat.common.wsgi:filter_factory
heat.filter_factory = heat.api.openstack:version_negotiation_filter

[filter:cors]
paste.filter_factory = oslo_middleware.cors:filter_factory
oslo_config_project = heat

[filter:faultwrap]
paste.filter_factory = heat.common.wsgi:filter_factory
heat.filter_factory = heat.api.openstack:faultwrap_filter

... (snip)

[filter:context]
paste.filter_factory = heat.common.context:ContextMiddleware_filter_factory

... (snip)

[filter:http_proxy_to_wsgi]
paste.filter_factory = oslo_middleware:HTTPProxyToWSGI.factory

# Middleware to set auth_url header appropriately
[filter:authurl]
paste.filter_factory = heat.common.auth_url:filter_factory

# Auth middleware that validates token against keystone
[filter:authtoken]
paste.filter_factory = keystonemiddleware.auth_token:filter_factory

... (snip)

# Middleware to set x-openstack-request-id in http response header
[filter:request_id]
paste.filter_factory = oslo_middleware.request_id:RequestId.factory

[filter:osprofiler]
paste.filter_factory = osprofiler.web:WsgiMiddleware.factory
```

この後の`deploy.loadapp`処理の開始に伴い、`FilterFactory`インスタンスの`__call__`メソッドが起動された場合には、_import_factoryメソッドが呼ばれる

- __version_negotiation_filter__の場合、`local_conf`には、`{'heat.filter_factory': 'heat.api.openstack:version_negotiation_filter'}`という情報が設定されている
- __faultwrap_filter__の場合、`local_conf`には、`{'heat.filter_factory': 'heat.api.openstack:faultwrap_filter'}`という情報が設定されている

### (2-2) [loadappメソッド](https://github.com/openstack/heat/blob/stable/stein/heat/common/wsgi.py#L1137)が起動される
deplory.loadappメソッド起動により、`api_paste.ini`ファイルの`[pipeline:heat-api]`セッションに記述された内容に従い、PasteDeployment処理が開始される。

```/etc/heat/api_paste.ini
# heat-api pipeline
[pipeline:heat-api]
pipeline = cors request_id faultwrap http_proxy_to_wsgi versionnegotiation authurl authtoken context osprofiler apiv1app

... (snip)
```
すなわち、 `apiv1app`  -> `osprofiler` -> `context` -> `authtoken` -> `authurl` ->  `versionnegotiation` -> `http_proxy_to_wsgi` -> `faultwrap` -> `request_id` -> `cors`という順番で、PasteDeployment処理が実施されることになる。


#### (2-2-1) `[app:apiv1app]`セクションの`heat.common.wsgi:app_factory`メソッドが起動される
`heat.common.wsgi:app_factory`メソッド起動により、[AppFactoryクラス](https://github.com/openstack/heat/blob/stable/stein/heat/common/wsgi.py#L1045-L1064)の`__call__`メソッドが起動された場合には ...

- `_import_factory`メソッドが呼ばれる
- `local_conf`には、`{'heat.app_factory': 'heat.api.openstack.v1:API'}`という情報が設定されている
- [heat.api.openstack.v1.APIクラス](https://github.com/openstack/heat/blob/stable/stein/heat/api/openstack/v1/__init__.py)が、インスタンス化されて、ルーティングのマッピング情報が設定される
- 上位クラスの[Router](https://github.com/openstack/heat/blob/stable/stein/heat/common/wsgi.py#L698-L748)のコンストラクタが起動されて、`self.map`には、ルーティングのマッピング情報が設定される
- [heat.api.openstack.v1.APIクラス](https://github.com/openstack/heat/blob/stable/stein/heat/api/openstack/v1/__init__.py)の`__call__`メソッド起動準備が完了したことになる

```python:heat/api/openstack/v1/__init__.py
class API(wsgi.Router):

    """WSGI router for Heat v1 REST API requests."""

    def __init__(self, conf, **local_conf):
        self.conf = conf
        mapper = routes.Mapper()
        default_resource = wsgi.Resource(wsgi.DefaultMethodController(),
                                         wsgi.JSONRequestDeserializer())

        def connect(controller, path_prefix, routes):
            """Connects list of routes to given controller with path_prefix.

            This function connects the list of routes to the given
            controller, prepending the given path_prefix. Then for each URL it
            finds which request methods aren't handled and configures those
            to return a 405 error. Finally, it adds a handler for the
            OPTIONS method to all URLs that returns the list of allowed
            methods with 204 status code.
            """
            # register the routes with the mapper, while keeping track of which
            # methods are defined for each URL
            urls = {}
            for r in routes:
                url = path_prefix + r['url']
                methods = r['method']
                if isinstance(methods, six.string_types):
                    methods = [methods]
                methods_str = ','.join(methods)
                mapper.connect(r['name'], url, controller=controller,
                               action=r['action'],
                               conditions={'method': methods_str})
                if url not in urls:
                    urls[url] = methods
                else:
                    urls[url] += methods

            # now register the missing methods to return 405s, and register
            # a handler for OPTIONS that returns the list of allowed methods
            for url, methods in urls.items():
                all_methods = ['HEAD', 'GET', 'POST', 'PUT', 'PATCH', 'DELETE']
                missing_methods = [m for m in all_methods if m not in methods]
                allowed_methods_str = ','.join(methods)
                mapper.connect(url,
                               controller=default_resource,
                               action='reject',
                               allowed_methods=allowed_methods_str,
                               conditions={'method': missing_methods})
                if 'OPTIONS' not in methods:
                    mapper.connect(url,
                                   controller=default_resource,
                                   action='options',
                                   allowed_methods=allowed_methods_str,
                                   conditions={'method': 'OPTIONS'})

        # Stacks
        stacks_resource = stacks.create_resource(conf)
        connect(controller=stacks_resource,
                path_prefix='/{tenant_id}',
                routes=[
                    # Template handling
                    {
                        'name': 'template_validate',
                        'url': '/validate',
                        'action': 'validate_template',
                        'method': 'POST'
                    },
                    {
                        'name': 'resource_types',
                        'url': '/resource_types',
                        'action': 'list_resource_types',
                        'method': 'GET'
                    },
                    {
                        'name': 'resource_schema',
                        'url': '/resource_types/{type_name}',
                        'action': 'resource_schema',
                        'method': 'GET'
                    },
                    {
                        'name': 'generate_template',
                        'url': '/resource_types/{type_name}/template',
                        'action': 'generate_template',
                        'method': 'GET'
                    },

                    {
                        'name': 'template_versions',
                        'url': '/template_versions',
                        'action': 'list_template_versions',
                        'method': 'GET'
                    },

                    {
                        'name': 'template_functions',
                        'url': '/template_versions/{template_version}'
                               '/functions',
                        'action': 'list_template_functions',
                        'method': 'GET'
                    },

                    # Stack collection
                    {
                        'name': 'stack_index',
                        'url': '/stacks',
                        'action': 'index',
                        'method': 'GET'
                    },
                    {
                        'name': 'stack_create',
                        'url': '/stacks',
                        'action': 'create',
                        'method': 'POST'
                    },
                    {
                        'name': 'stack_preview',
                        'url': '/stacks/preview',
                        'action': 'preview',
                        'method': 'POST'
                    },
                    {
                        'name': 'stack_detail',
                        'url': '/stacks/detail',
                        'action': 'detail',
                        'method': 'GET'
                    },

... snip

                ])
... snip

        super(API, self).__init__(mapper)
```

#### (2-2-2) `[filter:osprofiler]`セクションの`osprofiler.web:WsgiMiddleware.factory`メソッドが起動される
- [osprofiler.web.WsgiMiddleware.factory](https://github.com/openstack/osprofiler/blob/stable/stein/osprofiler/web.py#L92-L96)メソッドが起動される
- その場合、クロージャ`filter_`への引数`app`には、`heat.api.openstack.v1.API`インスタンスのオブジェクト値が渡される
- [WsgiMiddlewareクラス](https://github.com/openstack/osprofiler/blob/stable/stein/osprofiler/web.py#L69-L134)がインスタンス化されて、`self.application`には、`heat.api.openstack.v1.API`インスタンスのオブジェクト値が設定される
- [WsgiMiddlewareクラス](https://github.com/openstack/osprofiler/blob/stable/stein/osprofiler/web.py#L69-L134)の`__call__`メソッド起動準備が完了したことになる


#### (2-2-3) `[filter:context]`セクションの`heat.common.context:ContextMiddleware_filter_factory`メソッドが起動される
- [heat.common.context.ContextMiddleware_filter_factory](https://github.com/openstack/heat/blob/stable/stein/heat/common/context.py#L395-L403)メソッドが起動される
- その場合、クロージャ`filter`への引数`app`には、`osprofiler.web.WsgiMiddleware`インスタンスのオブジェクト値が渡される
- [ContextMiddlewareクラス](https://github.com/openstack/heat/blob/stable/stein/heat/common/context.py#L334-L392)がインスタンス化される
- 上位クラスの[Middlewareクラス](https://github.com/openstack/heat/blob/stable/stein/heat/common/wsgi.py#L602-L632)のコンストラクタが起動されて、`self.application`には、`osprofiler.web.WsgiMiddleware`インスタンスのオブジェクト値が設定される
- [ContextMiddlewareクラス](https://github.com/openstack/heat/blob/stable/stein/heat/common/context.py#L334-L392)の`__call__`メソッド起動準備が完了したことになる

#### (2-2-4) `[filter:authtoken]`セクションの`keystonemiddleware.auth_token:filter_factory`メソッドが起動される
- [keystonemiddleware.auth_token.filter_factory](https://github.com/openstack/keystonemiddleware/blob/stable/stein/keystonemiddleware/auth_token/__init__.py#L955-L962)メソッドが起動される
- その場合、クロージャ`filter`への引数`app`には、`heat.common.context.ContextMiddleware`インスタンスのオブジェクト値が渡される
- [AuthProtocolクラス](https://github.com/openstack/keystonemiddleware/blob/stable/stein/keystonemiddleware/auth_token/__init__.py#L535-L952)がインスタンス化されて、`self._app`には、`heat.common.context.ContextMiddleware`インスタンスのオブジェクト値が設定される
- [AuthProtocolクラス](https://github.com/openstack/keystonemiddleware/blob/stable/stein/keystonemiddleware/auth_token/__init__.py#L535-L952)の`__call__`メソッド起動準備が完了したことになる

#### (2-2-5) `[filter:authurl]`セクションの`heat.common.auth_url:filter_factory`メソッドが起動される
- [heat.common.auth_url.filter_factory](https://github.com/openstack/heat/blob/stable/stein/heat/common/auth_url.py#L65-L71)メソッドが起動される
- その場合、クロージャ`filter`への引数`app`には、`keystonemiddleware.auth_token.AuthProtocol`インスタンスのオブジェクト値が渡される
- [AuthUrlFilterクラス](https://github.com/openstack/heat/blob/stable/stein/heat/common/auth_url.py#L25-L62)がインスタンス化される
- Pythonモジュール`keystonemiddleware.auth_token`が、インポートされる
- `/etc/heat/heat.conf`ファイルの`[keystone_authtoken]`セクションに記述された`auth_uri`パラメータ値が取得される
- 上位クラスの[Middleware](https://github.com/openstack/heat/blob/stable/stein/heat/common/wsgi.py#L602-L632)のコンストラクタが起動されて、`self.application`には、`keystonemiddleware.auth_token.AuthProtocol`インスタンスのオブジェクト値が設定される
- [AuthUrlFilterクラス](https://github.com/openstack/heat/blob/stable/stein/heat/common/auth_url.py#L25-L62)の`__call__`メソッド起動準備が完了したことになる

#### (2-2-6) `[filter:versionnegotiation]`セクションの`heat.common.wsgi:filter_factory`メソッドが起動される
- `heat.common.wsgi.filter_factory`メソッド起動により、[FilterFactory クラス](https://github.com/openstack/heat/blob/stable/stein/heat/common/wsgi.py#L1067-L1090)の`__call__`メソッドが起動される
- `_import_factory`メソッドが呼ばれる
- `local_conf`には、`{'heat.filter_factory': 'heat.api.openstack:version_negotiation_filter'}`という情報が設定されている
- その場合、クロージャ`filter`への引数`app`には、`heat.common.auth_url.AuthUrlFilter`インスタンスのオブジェクト値が渡される
- [version_negotiation_filterメソッド](https://github.com/openstack/heat/blob/stable/stein/heat/api/openstack/__init__.py#L19-L21)が起動される
- [VersionNegotiationFilterクラス](https://github.com/openstack/heat/blob/stable/stein/heat/api/middleware/version_negotiation.py#L29-L110)がインスタンス化される
- 上位クラスの[Middleware](https://github.com/openstack/heat/blob/stable/stein/heat/common/wsgi.py#L602-L632)のコンストラクタが起動されて、`self.application`には、`heat.common.auth_url.AuthUrlFilter`インスタンスのオブジェクト値が設定される
- [VersionNegotiationFilterクラス](https://github.com/openstack/heat/blob/stable/stein/heat/api/middleware/version_negotiation.py#L29-L110)の`__call__`メソッド起動準備が完了したことになる

#### (2-2-7) `[filter:http_proxy_to_wsgi]`セクションの`oslo_middleware:HTTPProxyToWSGI.factory`メソッドが起動される
- [oslo_middleware.HTTPProxyToWSGI.factor](https://github.com/openstack/oslo.middleware/blob/stable/stein/oslo_middleware/base.py#L50-L67)メソッドが起動される
- その場合、クロージャ`middleware_filter`への引数`app`には、`heat.api.middleware.version_negotiation.VersionNegotiationFilter`インスタンスのオブジェクト値が渡される
- [HTTPProxyToWSGIクラス](https://github.com/openstack/oslo.middleware/blob/stable/stein/oslo_middleware/http_proxy_to_wsgi.py#L27-L93)がインスタンス化されて、`self.application`には、`heat.api.middleware.version_negotiation.VersionNegotiationFilter`インスタンスのオブジェクト値が設定される
- [HTTPProxyToWSGIクラス](https://github.com/openstack/oslo.middleware/blob/stable/stein/oslo_middleware/http_proxy_to_wsgi.py#L27-L93)の`__call__`メソッド起動準備が完了したことになる

#### (2-2-8) `[filter:faultwrap]`セクションの`heat.common.wsgi:filter_factory`メソッドが起動される
- `heat.common.wsgi.filter_factory`メソッド起動により、[FilterFactoryクラス](https://github.com/openstack/heat/blob/stable/stein/heat/common/wsgi.py#L1067-L1090)の`__call__`メソッドが起動される
- `_import_factory`メソッドが呼ばれる
- `local_conf`には、`{'heat.filter_factory': 'heat.api.openstack:faultwrap_filter'}`という情報が設定されている
- その場合、クロージャ`filter`への引数`app`には、`oslo_middleware.HTTPProxyToWSGI`インスタンスのオブジェクト値が渡される
- [faultwrap_filterメソッド](https://github.com/openstack/heat/blob/stable/stein/heat/api/openstack/__init__.py#L24-L25)が起動される
- [FaultWrapperクラス](https://github.com/openstack/heat/blob/stable/stein/heat/api/middleware/fault.py#L53-L170)がインスタンス化される
- 上位クラスの[Middleware](https://github.com/openstack/heat/blob/stable/stein/heat/common/wsgi.py#L602-L632)のコンストラクタが起動されて、`self.application`には、`oslo_middleware.HTTPProxyToWSGI`インスタンスのオブジェクト値が設定される
- [FaultWrapperクラス](https://github.com/openstack/heat/blob/stable/stein/heat/api/middleware/fault.py#L53-L170)の`__call__`メソッド起動準備が完了したことになる

#### (2-2-9) `[filter:request_id]`セクションの`oslo_middleware.request_id:RequestId.factory`メソッドが起動される
- [oslo_middleware.request_id.RequestId.factory](https://github.com/openstack/oslo.middleware/blob/stable/stein/oslo_middleware/base.py#L50-L67)メソッドが起動される
- その場合、クロージャ`middleware_filter`への引数`app`には、`heat.api.middleware.fault.FaultWrapper`インスタンスのオブジェクト値が渡される
- [RequestIdクラス](https://github.com/openstack/oslo.middleware/blob/stable/stein/oslo_middleware/request_id.py#L32-L66)がインスタンス化されて、`self.application`には、`heat.api.middleware.fault.FaultWrapper`インスタンスのオブジェクト値が設定される
- [RequestIdクラス](https://github.com/openstack/oslo.middleware/blob/stable/stein/oslo_middleware/request_id.py#L32-L66)の`__call__`メソッド起動準備が完了したことになる

#### (2-2-10) `[filter:cors]`セクションの`oslo_middleware.cors:filter_factory`メソッドが起動される
- [oslo_middleware.cors.filter_factory](https://github.com/openstack/oslo.middleware/blob/stable/stein/oslo_middleware/cors.py#L400)メソッドが起動される
- その場合、クロージャ`middleware_filter`への引数`app`には、`oslo_middleware.request_id.RequestId`インスタンスのオブジェクト値が渡される
- [CORSクラス](https://github.com/openstack/oslo.middleware/blob/stable/stein/oslo_middleware/cors.py#L100-L397)がインスタンス化されて、`self.application`には、`oslo_middleware.request_id.RequestId`インスタンスのオブジェクト値が設定される
- [CORSクラス](https://github.com/openstack/oslo.middleware/blob/stable/stein/oslo_middleware/cors.py#L100-L397)の`__call__`メソッド起動準備が完了したことになる

### (2-3) `paste_deploy_app`メソッド処理は、ここで完了となる。

## (3) wsgiサーバが起動される
`wsgi.Server`クラスが、インスタンス化された後、[wsgi.Server.start](https://github.com/openstack/heat/blob/stable/stein/heat/cmd/api.py)メソッドが起動される

```python:heat/cmd/api.py
def launch_api(setup_logging=True):
    if setup_logging:
        logging.register_options(cfg.CONF)
    cfg.CONF(project='heat', prog='heat-api',
             version=version.version_info.version_string())
    if setup_logging:
        logging.setup(cfg.CONF, 'heat-api')
    config.set_config_defaults()
    messaging.setup()

    app = config.load_paste_app()

    port = cfg.CONF.heat_api.bind_port
    host = cfg.CONF.heat_api.bind_host
    LOG.info('Starting Heat REST API on %(host)s:%(port)s',
             {'host': host, 'port': port})
    profiler.setup('heat-api', host)
    gmr.TextGuruMeditation.setup_autorun(version)
    server = wsgi.Server('heat-api', cfg.CONF.heat_api)
    server.start(app, default_port=port)
    return server


def main():
    try:
        server = launch_api()
        systemd.notify_once()
        server.wait()
    except RuntimeError as e:
        msg = six.text_type(e)
        sys.exit("ERROR: %s" % msg)
```
`/etc/heat/heat.conf`ファイルに、workersパラメータを設定することが可能だが、["Heat Configuration Sample"](https://docs.openstack.org/heat/stein/configuration/sample_config.html)によると、デフォルトでは、設定されていないので、デフォルト"0"として扱われる。

```
[heat_api]
...

# Number of workers for Heat service. Default value 0 means, that service will
# start number of workers equal number of cores on server. (integer value)
# Minimum value: 0
#workers = 0
```

そして、`workers = 0`の場合には、[run_server](https://github.com/openstack/heat/blob/stable/stein/heat/common/wsgi.py#L564-L590)メソッドが起動される

```python:heat/heat/common/wsgi.py
    def run_server(self):
        """Run a WSGI server."""
        eventlet.wsgi.HttpProtocol.default_request_version = "HTTP/1.0"
        eventlet.hubs.use_hub('poll')
        eventlet.patcher.monkey_patch(all=False, socket=True)
        self.pool = eventlet.GreenPool(size=self.threads)
        socket_timeout = cfg.CONF.eventlet_opts.client_socket_timeout or None

        # Close write to ensure only parent has it open
        os.close(self.writepipe)
        # Create greenthread to watch for parent to close pipe
        eventlet.spawn_n(self._pipe_watcher)

        try:
            eventlet.wsgi.server(
                self.sock,
                self.application,
                custom_pool=self.pool,
                url_length_limit=URL_LENGTH_LIMIT,
                log=self._logger,
                debug=cfg.CONF.debug,
                keepalive=cfg.CONF.eventlet_opts.wsgi_keep_alive,
                socket_timeout=socket_timeout)
        except socket.error as err:
            if err[0] != errno.EINVAL:
                raise
        self.pool.waitall()
```

# ◼️ heat-apiのリクエスト受け付け処理の動作メカニズムを調査してみる
つづいて、heat-apiでのリクエスト受け付け処理の流れを、確認していきます。

## (1) [oslo_middleware.cors.CORSクラス](https://github.com/openstack/oslo.middleware/blob/stable/stein/oslo_middleware/cors.py#L100-L397)の`__call__`メソッドが起動される

- `CORSクラス`の[`__call__`メソッド](https://github.com/openstack/oslo.middleware/blob/stable/stein/oslo_middleware/base.py#L126-L136)が起動される
- `self.application`には、`oslo_middleware.request_id.RequestId`インスタンスのオブジェクト値が設定されるため、RequestIdクラスの`__call__`メソッド`を呼び出すことになる

## (2) [oslo_middleware.request_id.RequestIdクラス](https://github.com/openstack/oslo.middleware/blob/stable/stein/oslo_middleware/request_id.py#L32-L66)の`__call__`メソッドが起動される

- [RequestIdクラス](https://github.com/openstack/oslo.middleware/blob/stable/stein/oslo_middleware/request_id.py#L32-L66)の`__call__`メソッドが起動される
- `req_id`が、新たに生成される
- `req.environ['openstack.request_id']`に、新たに生成された`req_id`が設定される
- `self.application`には、`heat.api.middleware.fault.FaultWrapper`インスタンスのオブジェクト値が設定されるため、FaultWrapperクラスの`__call__`メソッド`を呼び出すことになる
- レスポンスメッセージのヘッダ情報に、`x-openstack-request-id`パラメータとして追加される

## (3) [heat.api.middleware.fault.FaultWrapperクラス](https://github.com/openstack/heat/blob/stable/stein/heat/api/middleware/fault.py#L53-L170)の`__call__`メソッドが起動される

- `FaultWrapperクラス`の[`__call__`メソッド](https://github.com/openstack/heat/blob/stable/stein/heat/common/wsgi.py#L626-L632)が起動される
- `FaultWrapperクラス`の[`process_request`メソッド](https://github.com/openstack/heat/blob/stable/stein/heat/api/middleware/fault.py#L166-L170)が起動される
- `self.application`には、`oslo_middleware.HTTPProxyToWSGI`インスタンスのオブジェクト値が設定されるため、HTTPProxyToWSGIクラスの`__call__`メソッド`を呼び出すことになる

## (4) [oslo_middleware.HTTPProxyToWSGIクラス](https://github.com/openstack/oslo.middleware/blob/stable/stein/oslo_middleware/http_proxy_to_wsgi.py#L27-L93)の`__call__`メソッドが起動される

- `HTTPProxyToWSGIクラス`の[`__call__`メソッド](https://github.com/openstack/oslo.middleware/blob/stable/stein/oslo_middleware/base.py#L126-L136)が起動される
- `HTTPProxyToWSGIクラス`の[`process_request`メソッド](https://github.com/openstack/oslo.middleware/blob/stable/stein/oslo_middleware/http_proxy_to_wsgi.py#L55-L93)が起動される
- `self.application`には、`heat.api.middleware.version_negotiation.VersionNegotiationFilter`インスタンスのオブジェクト値が設定されるため、VersionNegotiationFilterクラスの`__call__`メソッド`を呼び出すことになる

## (5) [heat.api.middleware.version_negotiation.VersionNegotiationFilterクラス](https://github.com/openstack/heat/blob/stable/stein/heat/api/middleware/version_negotiation.py#L29-L130)の`__call__`メソッドが起動される

- `VersionNegotiationFilterクラス`の[`__call__`メソッド](https://github.com/openstack/heat/blob/stable/stein/heat/common/wsgi.py#L626-L632)が起動される
- `VersionNegotiationFilterクラス`の[`process_request`メソッド](https://github.com/openstack/heat/blob/stable/stein/heat/api/middleware/version_negotiation.py#L37-L110)が起動される
- [req.path_info_peek](https://docs.pylonsproject.org/projects/webob/en/stable/reference.html)メソッドを用いて、`req.environ['PATH_INFO']`に、バージョン番号が含まれていなければ、リクエストメッセージに対するバージョンチェックが行われる
- バージョンチェックで問題なければ、デバックログとして、__"Matched versioned URI. Version: 1.0"__と出力される
- `self.application`には、`heat.common.auth_url.AuthUrlFilter`インスタンスのオブジェクト値が設定されるため、AuthUrlFilterクラスの`__call__`メソッド`を呼び出すことになる

## (6) [heat.common.auth_url.AuthUrlFilterクラス](https://github.com/openstack/heat/blob/stable/stein/heat/common/auth_url.py#L25-L62)の`__call__`メソッドが起動される

- `AuthUrlFilterクラス`の[`__call__`メソッド](https://github.com/openstack/heat/blob/stable/stein/heat/common/wsgi.py#L626-L632)が起動される
- `AuthUrlFilterクラス`の[`process_request`メソッド](https://github.com/openstack/heat/blob/stable/stein/heat/common/auth_url.py#L55-L62)が起動される
- `req.headers['X-Auth-Url']`に、`self.auth_url`が設定される（すなわち、リクエスト受け付け毎に、`/etc/heat/heat.conf`ファイルの`[keystone_authtoken]`セクションの`auth_uri`パラメータ値が引用される）

```/etc/heat/heat.conf
[keystone_authtoken]

auth_uri = http://<keystone-server>:5000
```
- `self.application`には、`keystonemiddleware.auth_token.AuthProtocol`インスタンスのオブジェクト値が設定されるため、AuthProtocolクラスの`__call__`メソッド`を呼び出すことになる

## (7) [keystonemiddleware.auth_token.AuthProtocolクラス](https://github.com/openstack/keystonemiddleware/blob/stable/stein/keystonemiddleware/auth_token/__init__.py#L535-L952)の`__call__`メソッドが起動される

- `AuthProtocolクラス`の[`__call__`メソッド](https://github.com/openstack/keystonemiddleware/blob/stable/stein/keystonemiddleware/auth_token/__init__.py#L327-L334)が起動される
- `AuthProtocolクラス`の[`process_request`メソッド](https://github.com/openstack/keystonemiddleware/blob/stable/stein/keystonemiddleware/auth_token/__init__.py#L598-L670)が起動される
- auth_token処理が開始される
- `self._app`には、`heat.common.context.ContextMiddleware`インスタンスのオブジェクト値が設定されるため、ContextMiddlewareクラスの`__call__`メソッド`を呼び出すことになる

## (8) [heat.common.context.ContextMiddlewareクラス](https://github.com/openstack/heat/blob/stable/stein/heat/common/context.py#L334-L392)の`__call__`メソッドが起動される

- `ContextMiddlewareクラス`の[`__call__`メソッド](https://github.com/openstack/heat/blob/stable/stein/heat/common/wsgi.py#L626-L632)が起動される
- `ContextMiddlewareクラス`の[`process_request`メソッド](https://github.com/openstack/heat/blob/stable/stein/heat/common/context.py#L344-L392)が起動される
- context情報が生成される
- `self.application`には、`osprofiler.web.WsgiMiddleware`インスタンスのオブジェクト値が設定されるため、WsgiMiddlewareクラスの`__call__`メソッド`を呼び出すことになる

## (9) [osprofiler.web.WsgiMiddlewareクラス](https://github.com/openstack/osprofiler/blob/stable/stein/osprofiler/web.py#L69-L134)の`__call__`メソッドが起動される

- `WsgiMiddlewareクラス`の[`__call__`メソッド](https://github.com/openstack/osprofiler/blob/stable/stein/osprofiler/web.py#L108-L134)が起動される
- `self.application`には、`heat.api.openstack.v1.API`インスタンスのオブジェクト値が設定されるため、heat.api.openstack.v1.APIクラスの`__call__`メソッド`を呼び出すことになる

## (10) [heat.api.openstack.v1.APIクラス](https://github.com/openstack/heat/blob/stable/stein/heat/api/openstack/v1/__init__.py#L28-L485)の`__call__`メソッドが起動される

- `heat.api.openstack.v1.APIクラス`の[`__call__`メソッド](https://github.com/openstack/heat/blob/stable/stein/heat/common/wsgi.py#L727-L733)が起動される
- リクエストメッセージ受け付け処理として、ルーティングのマッピング情報との比較により、その後の継続処理が決定される
- そして、適切な[policy処理](https://github.com/openstack/heat/blob/stable/stein/heat/common/policy.py)が施される
- [StackController](https://github.com/openstack/heat/blob/stable/stein/heat/api/openstack/v1/stacks.py#L171-L748)クラスで定義されたメソッドが起動される

## (11) heat-engineへの依頼すべき、メッセージ生成が完了した段階で、RabbitMQを経由して、heat-engineに情報伝達される。
なお、メッセージング処理機構は、Qiita記事「[OpenStackメッセージング機構を探ってみる](https://qiita.com/ttsubo/items/1f2884d41c3fe3129c5e)」を参照ください。


# ◼️ おわりに
heat-api内部構造を説明するにあたり、結構、複雑な文章になってしまった。
ただし、大まかなheat-api処理は、Qiita記事「[OpenStackを活用して、WSGI アプリケーションの仕組みを理解する](https://qiita.com/ttsubo/items/0a1abecce272559edc94)」で記述したサンプルアプリと、やっている事は、概ね同じです。
根気を持って確認してもらえれば、理解は難しくないと思います。



# ◼️ 参考文献
## (1) Paste Deployment
- [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-アプリケーションを設定する)


## (2) WebOb
- [WebOb Reference](https://docs.pylonsproject.org/projects/webob/en/stable/reference.html)
- [Python の WebOb で WSGI のリクエスト・レスポンスをカプセル化する](http://momijiame.tumblr.com/post/50145128041/python-の-webob-で-wsgi-のリクエストレスポンスをカプセル化する)

## (3) その他
- [Python の Routes で HTTP リクエストをルーティングする](http://momijiame.tumblr.com/post/29400971777/python-の-routes-で-http-リクエストをルーティングする)