OpenStackで使用されているプラグイン機構Stevedoreの使い方

概要

OpenStackでは、プラグイン機構を採用しており、バックエンドの実装にさまざまなものを選択できるようにしている

たとえば、NeutronではCore pluginに「ml2, openvswitch, nsx」などなど、さまざまなpluginが設定ファイルで設定できる。それをstevedoreというライブラリを用いて実現可能にしている。

今回はそれの使い方について、keystoneclientのauth_pluginの読み込み部分を見ながら使い方を見ていく

※注意:イマイチ良く分かっていないところがあるので、間違いや詳細が分かる方はコメントください

keystoneclientにauth_pluginについて(stevedoreの使用例)

keystoneclientでは、認証の方法をplugin形式で実装している(Token,Password等)ので、さまざまな認証方法を選択することができる。

実際に、keystoneclientを利用しているkeystonemiddlewareのauth_token( WSGIアプリケーションのFilterでは、設定ファイルで、使用するkeystonclientで読み込むauth_pluginを設定ファイルで下記のように、指定している。

devstackによって生成される、nova.conf

52
53 [keystone_authtoken]
54 signing_dir = /var/cache/nova
55 cafile = /opt/stack/data/ca-bundle.pem
56 auth_uri = http://157.7.84.233:5000
57 project_domain_id = default
58 project_name = service
59 user_domain_id = default
60 password = password
61 username = nova
62 auth_url = http://157.7.84.233:35357
63 auth_plugin = password
64

この設定だとkeystonemiddlewareのauth_tokenでkeystoneclientを作成するときにPassword認証用のプラグインで作成するようになります

/usr/local/lib/python2.7/dist-packages/keystonemiddleware/auth_token.py

176 import logging
177 import os
178 import stat
179 import tempfile
180
181 from keystoneclient import access
182 from keystoneclient import adapter
183 from keystoneclient import auth
184 from keystoneclient.auth.identity import base as base_identity
185 from keystoneclient.auth.identity import v2
186 from keystoneclient.auth import token_endpoint
~~~~~~~~~~~~
~~~~~~~~~~~~
825
826 class AuthProtocol(object):
~~~~~~~~~~~~
~~~~~~~~~~~~
1494 def _create_identity_server(self):
1495 # NOTE(jamielennox): Loading Session here should be exactly the
1496 # same as calling Session.load_from_conf_options(CONF, GROUP)
1497 # however we can't do that because we have to use _conf_get to
1498 # support the paste.ini options.
1499 sess = session.Session.construct(dict(
1500 cert=self._conf_get('certfile'),
1501 key=self._conf_get('keyfile'),
1502 cacert=self._conf_get('cafile'),
1503 insecure=self._conf_get('insecure'),
1504 timeout=self._conf_get('http_connect_timeout')
1505 ))
1506
1507 # NOTE(jamielennox): The original auth mechanism allowed deployers
1508 # to configure authentication information via paste file. These
1509 # are accessible via _conf_get, however this doesn't work with the
1510 # plugin loading mechanisms. For using auth plugins we only support
1511 # configuring via the CONF file.
1512 auth_plugin = auth.load_from_conf_options(CONF, _AUTHTOKEN_GROUP)
1513
~~~~~~~~~~~~
~~~~~~~~~~~~

このAuthProtocolクラスというのが、tokenチェックをするためのWsgiアプリケーションのfilterに当たるのですが、そのなかで_create_identity_server関数定義(keystoneclientを作るような処理をする)のなかでauth_pluginのpluginを読み込んでいます(1512行目)「auth.load_from_conf_options」の関数の中身を見ていきます

/usr/local/lib/python2.7/dist-packages/keystoneclient/auth/conf.py

79
80 def load_from_conf_options(conf, group, **kwargs):
81 """Load a plugin from an oslo.config CONF object.
~~~~~~
commentのため、割愛
~~~~~~
98 """
99 # NOTE(jamielennox): plugins are allowed to specify a 'section' which is
100 # the group that auth options should be taken from. If not present they
101 # come from the same as the base options were registered in.
102 if conf[group].auth_section:
103 group = conf[group].auth_section
104
105 name = conf[group].auth_plugin
106 if not name:
107 return None
108
109 plugin_class = base.get_plugin_class(name)
110 plugin_class.register_conf_options(conf, group)
111 return plugin_class.load_from_conf_options(conf, group, **kwargs)
~~~~~~
~~~~~~

ここでは、pluginクラスを生成して、生成したpluginクラスに設定をつっこんでます。実際に、stevedoreを使ってpluginを生成しているのbase.get_plugin_classのところ(109行目)、これも関数の中身を見て行きます

/usr/local/lib/python2.7/dist-packages/keystoneclient/auth/base.py

15 import six
16 import stevedore
~~~~~
~~~~~
26 PLUGIN_NAMESPACE = 'keystoneclient.auth.plugin'
27 IDENTITY_AUTH_HEADER_NAME = 'X-Auth-Token'
28
29
30 def get_plugin_class(name):
31 """Retrieve a plugin class by its entrypoint name.
32
33 :param str name: The name of the object to get.
34
35 :returns: An auth plugin class.
36 :rtype: :py:class:`keystoneclient.auth.BaseAuthPlugin`
37
38 :raises keystoneclient.exceptions.NoMatchingPlugin: if a plugin cannot be
39 created.
40 """
41 try:
42 mgr = stevedore.DriverManager(namespace=PLUGIN_NAMESPACE,
43 name=name,
44 invoke_on_load=False)
45 except RuntimeError:
46 raise exceptions.NoMatchingPlugin(name)
47
48 return mgr.driver
~~~~~~~
~~~~~~~

やっとここで、stevedoreの登場です(42行目)。 stevedore.DriverManagerが与えられたname(このときはpassword)に対応するクラスを探して、読み込みます。

stevedoreについて

先ほどまで、keytonemiddlewareとkeystoneclientを見て実際にプラグインが読み込まれる過程を見ました。 ここから、stevedoreの使い方を見ていきます。

上記の例を見ても分かるように、stevedoreではpluginクラスをフルクラスネームで書かなくても、読み込まれます(例: password → keystoneclient.auth.identity.generic:Password )

なぜそんなことができるかというと、インストール時にsetup.cfgでそのマッピングについて定義するからです。 その定義がこちら

keystoneclientのsetup.cfg

[entry_points]
`console_scripts =
keystone = keystoneclient.shell:main
keystoneclient.auth.plugin =
password = keystoneclient.auth.identity.generic:Password
token = keystoneclient.auth.identity.generic:Token
v2password = keystoneclient.auth.identity.v2:Password
v2token = keystoneclient.auth.identity.v2:Token
v3password = keystoneclient.auth.identity.v3:Password
v3token = keystoneclient.auth.identity.v3:Token
v3unscopedsaml = keystoneclient.contrib.auth.v3.saml2:Saml2UnscopedToken
v3scopedsaml = keystoneclient.contrib.auth.v3.saml2:Saml2ScopedToken
v3unscopedadfs = keystoneclient.contrib.auth.v3.saml2:ADFSUnscopedToken

ここでは、console_scriptskeystoneclient.auth.pluginはnamespaceといい、インデントが違うpasswordtokenがnameといいます。ここまでくれば分かるかと思いますが。 namespaceとnameの組み合わせで、対応クラスがマッピングされます。

例えば、namespace = keystoneclient.auth.plugin , name = passwordとすれば、読み込まれるクラスは、keystoneclient.auth.identity.generic:Passwordになります。

また、rpmdebパッケージからインストールするとsetup.cfgが見当たらないかと思いますが、その場合は /usr/local/lib/python2.7/dist-packages/XXXXX.dist-info の中のentry_points.txtに同じような定義があります。 例として、keystoneclientのentry_points.txtを下記に記します。

/usr/local/lib/python2.7/dist-packages/python_keystoneclient-1.1.0.dist-info/entry_points.txt

1 [console_scripts]
2 keystone = keystoneclient.shell:main
3
4 [keystoneclient.auth.plugin]
5 password = keystoneclient.auth.identity.generic:Password
6 token = keystoneclient.auth.identity.generic:Token
7 v2password = keystoneclient.auth.identity.v2:Password
8 v2token = keystoneclient.auth.identity.v2:Token
9 v3password = keystoneclient.auth.identity.v3:Password
10 v3scopedsaml = keystoneclient.contrib.auth.v3.saml2:Saml2ScopedToken
11 v3token = keystoneclient.auth.identity.v3:Token
12 v3unscopedadfs = keystoneclient.contrib.auth.v3.saml2:ADFSUnscopedToken
13 v3unscopedsaml = keystoneclient.contrib.auth.v3.saml2:Saml2UnscopedToken

表記は、違いますが雰囲気で分かると思います。 で囲まれている部分がnamespaceに対応し、の下に書かれている部分がnameに相当します

neutronのpluginの読まれ方

ここで疑問に、思う方がいるかもしれませんが(僕は思いました)。password,tokenなどnameを使用して、entry_pointsでそのマッピング先を探すのに、設定ファイルでクラスパスを直接書いている部分があるのはなぜだろう。stevedoreは、nameにクラスパスが指定されていたらそのクラスパスのクラスを読み込むのか?

例えばneutronでは、service_pluginsの指定に、フルクラスネームで書くこともnameで書くこともできます。

neutron.conf

[DEFAULT]
service_plugins = router

neutron.conf

[DEFAULT]
service_plugins = neutron.services.l3_router.l3_router_plugin.L3RouterPlugin

neutronのpluginの読み込み方を調べてみたところ、こんな風になっていました。

/opt/stack/neutron/neutron/manager.py

130 def _get_plugin_instance(self, namespace, plugin_provider):
131 try:
132 # Try to resolve plugin by name
133 mgr = driver.DriverManager(namespace, plugin_provider)
134 plugin_class = mgr.driver
135 except RuntimeError as e1:
136 # fallback to class name
137 try:
138 plugin_class = importutils.import_class(plugin_provider)
139 except ImportError as e2:
140 LOG.exception(_LE("Error loading plugin by name, %s"), e1)
141 LOG.exception(_LE("Error loading plugin by class, %s"), e2)
142 raise ImportError(_("Plugin not found."))
143 return plugin_class()

stevedoreは、nameにマッチするentry_pointが見つからない場合は、例外を吐くのでその例外をキャッチし、クラスパスが指定されていることを仮定し、importutilsでimportを試みてるんですね。

なので、設定ファイルのpluginに指定する値は、entry_pointのnameでも、クラスパスでもうまくいくんですね。面白い