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_scripts
とkeystoneclient.auth.plugin
はnamespaceといい、インデントが違うpassword
やtoken
がnameといいます。ここまでくれば分かるかと思いますが。
namespaceとnameの組み合わせで、対応クラスがマッピングされます。
例えば、namespace = keystoneclient.auth.plugin , name = passwordとすれば、読み込まれるクラスは、keystoneclient.auth.identity.generic:Passwordになります。
また、rpmやdebパッケージからインストールすると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でも、クラスパスでもうまくいくんですね。面白い