LoginSignup
1
1

More than 5 years have passed since last update.

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

Posted at

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

◼️ そもそも、OpenStack heatって、どんなもの?

Qiita記事「Mitaka版 OpenStack Heat環境をセットアップしてみた」が参考になると思います。

◼️ OpenStack heatでの、大まかな処理の流れ

OpenStack heatは、リクエストに応じて、適切なクラウドオペレーション(novaインスタンス作成など)を行います。

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

なお、メッセージング処理機構は、Qiita記事「OpenStackメッセージング機構を探ってみる」を参照ください。

Heat Architecture.002.png

(1) heat-api起動の流れは、どうなっているのか?

こちらのスライド"OpenStack API's and WSGI"からの引用です。
名称未設定.001.png
今回、heat-api動作の流れを調査する上で、これらのキーワードを意識しておくことが大切になりそうです。

(2) heat向けのリクエストを、どうやって処理しているのか?

heat-api内部にて、受け付けたリクエストメッセージを適切に処理する場合、WSGI アプリケーションの機構が関与しているようです。そこで、まずは、WSGI アプリケーションの仕組みを理解する必要があります。
過去のQiita記事「OpenStackを活用して、WSGI アプリケーションの仕組みを理解するを参照ください。

◼️ OpenStack heat環境として、/etc/heat/api_paste.iniファイルを事前確認しておく

/etc/heat/api_paste.ini が配備されているので、事前に内容を確認しておく

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"のPythonスクリプト動作の流れを、確認していきます。

(1) load_paste_appメソッドが起動される

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

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"が設定される

(1-1) _get_deployment_flavorメソッドが起動される

/etc/heat/heat.confファイルに、flavorパラメータを設定することが可能だが、"Heat Configuration Sample"によると、デフォルトでは、設定されていないので、特に、何もしない。

[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"ファイルの保管先としてパス情報が設定される。

(2) paste_deploy_appメソッドが起動される

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

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メソッドが起動される

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

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クラスが呼び出される

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

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クラスが呼び出される

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

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メソッドが起動される

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クラス__call__メソッドが起動された場合には ...

  • _import_factoryメソッドが呼ばれる
  • local_confには、{'heat.app_factory': 'heat.api.openstack.v1:API'}という情報が設定されている
  • heat.api.openstack.v1.APIクラスが、インスタンス化されて、ルーティングのマッピング情報が設定される
  • 上位クラスのRouterのコンストラクタが起動されて、self.mapには、ルーティングのマッピング情報が設定される
  • heat.api.openstack.v1.APIクラス__call__メソッド起動準備が完了したことになる
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メソッドが起動される
  • その場合、クロージャfilter_への引数appには、heat.api.openstack.v1.APIインスタンスのオブジェクト値が渡される
  • WsgiMiddlewareクラスがインスタンス化されて、self.applicationには、heat.api.openstack.v1.APIインスタンスのオブジェクト値が設定される
  • WsgiMiddlewareクラス__call__メソッド起動準備が完了したことになる

(2-2-3) [filter:context]セクションのheat.common.context:ContextMiddleware_filter_factoryメソッドが起動される

(2-2-4) [filter:authtoken]セクションのkeystonemiddleware.auth_token:filter_factoryメソッドが起動される

  • keystonemiddleware.auth_token.filter_factoryメソッドが起動される
  • その場合、クロージャfilterへの引数appには、heat.common.context.ContextMiddlewareインスタンスのオブジェクト値が渡される
  • AuthProtocolクラスがインスタンス化されて、self._appには、heat.common.context.ContextMiddlewareインスタンスのオブジェクト値が設定される
  • AuthProtocolクラス__call__メソッド起動準備が完了したことになる

(2-2-5) [filter:authurl]セクションのheat.common.auth_url:filter_factoryメソッドが起動される

  • heat.common.auth_url.filter_factoryメソッドが起動される
  • その場合、クロージャfilterへの引数appには、keystonemiddleware.auth_token.AuthProtocolインスタンスのオブジェクト値が渡される
  • AuthUrlFilterクラスがインスタンス化される
  • Pythonモジュールkeystonemiddleware.auth_tokenが、インポートされる
  • /etc/heat/heat.confファイルの[keystone_authtoken]セクションに記述されたauth_uriパラメータ値が取得される
  • 上位クラスのMiddlewareのコンストラクタが起動されて、self.applicationには、keystonemiddleware.auth_token.AuthProtocolインスタンスのオブジェクト値が設定される
  • AuthUrlFilterクラス__call__メソッド起動準備が完了したことになる

(2-2-6) [filter:versionnegotiation]セクションのheat.common.wsgi:filter_factoryメソッドが起動される

  • heat.common.wsgi.filter_factoryメソッド起動により、FilterFactory クラス__call__メソッドが起動される
  • _import_factoryメソッドが呼ばれる
  • local_confには、{'heat.filter_factory': 'heat.api.openstack:version_negotiation_filter'}という情報が設定されている
  • その場合、クロージャfilterへの引数appには、heat.common.auth_url.AuthUrlFilterインスタンスのオブジェクト値が渡される
  • version_negotiation_filterメソッドが起動される
  • VersionNegotiationFilterクラスがインスタンス化される
  • 上位クラスのMiddlewareのコンストラクタが起動されて、self.applicationには、heat.common.auth_url.AuthUrlFilterインスタンスのオブジェクト値が設定される
  • VersionNegotiationFilterクラス__call__メソッド起動準備が完了したことになる

(2-2-7) [filter:http_proxy_to_wsgi]セクションのoslo_middleware:HTTPProxyToWSGI.factoryメソッドが起動される

  • oslo_middleware.HTTPProxyToWSGI.factorメソッドが起動される
  • その場合、クロージャmiddleware_filterへの引数appには、heat.api.middleware.version_negotiation.VersionNegotiationFilterインスタンスのオブジェクト値が渡される
  • HTTPProxyToWSGIクラスがインスタンス化されて、self.applicationには、heat.api.middleware.version_negotiation.VersionNegotiationFilterインスタンスのオブジェクト値が設定される
  • HTTPProxyToWSGIクラス__call__メソッド起動準備が完了したことになる

(2-2-8) [filter:faultwrap]セクションのheat.common.wsgi:filter_factoryメソッドが起動される

  • heat.common.wsgi.filter_factoryメソッド起動により、FilterFactoryクラス__call__メソッドが起動される
  • _import_factoryメソッドが呼ばれる
  • local_confには、{'heat.filter_factory': 'heat.api.openstack:faultwrap_filter'}という情報が設定されている
  • その場合、クロージャfilterへの引数appには、oslo_middleware.HTTPProxyToWSGIインスタンスのオブジェクト値が渡される
  • faultwrap_filterメソッドが起動される
  • FaultWrapperクラスがインスタンス化される
  • 上位クラスのMiddlewareのコンストラクタが起動されて、self.applicationには、oslo_middleware.HTTPProxyToWSGIインスタンスのオブジェクト値が設定される
  • FaultWrapperクラス__call__メソッド起動準備が完了したことになる

(2-2-9) [filter:request_id]セクションのoslo_middleware.request_id:RequestId.factoryメソッドが起動される

  • oslo_middleware.request_id.RequestId.factoryメソッドが起動される
  • その場合、クロージャmiddleware_filterへの引数appには、heat.api.middleware.fault.FaultWrapperインスタンスのオブジェクト値が渡される
  • RequestIdクラスがインスタンス化されて、self.applicationには、heat.api.middleware.fault.FaultWrapperインスタンスのオブジェクト値が設定される
  • RequestIdクラス__call__メソッド起動準備が完了したことになる

(2-2-10) [filter:cors]セクションのoslo_middleware.cors:filter_factoryメソッドが起動される

  • oslo_middleware.cors.filter_factoryメソッドが起動される
  • その場合、クロージャmiddleware_filterへの引数appには、oslo_middleware.request_id.RequestIdインスタンスのオブジェクト値が渡される
  • CORSクラスがインスタンス化されて、self.applicationには、oslo_middleware.request_id.RequestIdインスタンスのオブジェクト値が設定される
  • CORSクラス__call__メソッド起動準備が完了したことになる

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

(3) wsgiサーバが起動される

wsgi.Serverクラスが、インスタンス化された後、wsgi.Server.startメソッドが起動される

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"によると、デフォルトでは、設定されていないので、デフォルト"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メソッドが起動される

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クラス__call__メソッドが起動される

  • CORSクラス__call__メソッドが起動される
  • self.applicationには、oslo_middleware.request_id.RequestIdインスタンスのオブジェクト値が設定されるため、RequestIdクラスの__call__メソッド`を呼び出すことになる

(2) oslo_middleware.request_id.RequestIdクラス__call__メソッドが起動される

  • RequestIdクラス__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クラス__call__メソッドが起動される

  • FaultWrapperクラス__call__メソッドが起動される
  • FaultWrapperクラスprocess_requestメソッドが起動される
  • self.applicationには、oslo_middleware.HTTPProxyToWSGIインスタンスのオブジェクト値が設定されるため、HTTPProxyToWSGIクラスの__call__メソッド`を呼び出すことになる

(4) oslo_middleware.HTTPProxyToWSGIクラス__call__メソッドが起動される

  • HTTPProxyToWSGIクラス__call__メソッドが起動される
  • HTTPProxyToWSGIクラスprocess_requestメソッドが起動される
  • self.applicationには、heat.api.middleware.version_negotiation.VersionNegotiationFilterインスタンスのオブジェクト値が設定されるため、VersionNegotiationFilterクラスの__call__メソッド`を呼び出すことになる

(5) heat.api.middleware.version_negotiation.VersionNegotiationFilterクラス__call__メソッドが起動される

  • VersionNegotiationFilterクラス__call__メソッドが起動される
  • VersionNegotiationFilterクラスprocess_requestメソッドが起動される
  • req.path_info_peekメソッドを用いて、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クラス__call__メソッドが起動される

  • AuthUrlFilterクラス__call__メソッドが起動される
  • AuthUrlFilterクラスprocess_requestメソッドが起動される
  • 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クラス__call__メソッドが起動される

  • AuthProtocolクラス__call__メソッドが起動される
  • AuthProtocolクラスprocess_requestメソッドが起動される
  • auth_token処理が開始される
  • self._appには、heat.common.context.ContextMiddlewareインスタンスのオブジェクト値が設定されるため、ContextMiddlewareクラスの__call__メソッド`を呼び出すことになる

(8) heat.common.context.ContextMiddlewareクラス__call__メソッドが起動される

  • ContextMiddlewareクラス__call__メソッドが起動される
  • ContextMiddlewareクラスprocess_requestメソッドが起動される
  • context情報が生成される
  • self.applicationには、osprofiler.web.WsgiMiddlewareインスタンスのオブジェクト値が設定されるため、WsgiMiddlewareクラスの__call__メソッド`を呼び出すことになる

(9) osprofiler.web.WsgiMiddlewareクラス__call__メソッドが起動される

  • WsgiMiddlewareクラス__call__メソッドが起動される
  • self.applicationには、heat.api.openstack.v1.APIインスタンスのオブジェクト値が設定されるため、heat.api.openstack.v1.APIクラスの__call__メソッド`を呼び出すことになる

(10) heat.api.openstack.v1.APIクラス__call__メソッドが起動される

  • heat.api.openstack.v1.APIクラス__call__メソッドが起動される
  • リクエストメッセージ受け付け処理として、ルーティングのマッピング情報との比較により、その後の継続処理が決定される
  • そして、適切なpolicy処理が施される
  • StackControllerクラスで定義されたメソッドが起動される

(11) heat-engineへの依頼すべき、メッセージ生成が完了した段階で、RabbitMQを経由して、heat-engineに情報伝達される。

なお、メッセージング処理機構は、Qiita記事「OpenStackメッセージング機構を探ってみる」を参照ください。

◼️ おわりに

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

◼️ 参考文献

(1) Paste Deployment

(2) WebOb

(3) その他

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