TL; DR
- pyVmomiでは、プロパティを使用して属性参照時にAPIリクエストされる
はじめに
pyVmomiは、PythonでVMware vSphere Management APIを扱うためのSDKです。
同様のSDKは他言語でも存在し、例えばGo言語版のgovmomiが提供されています。
pyVmomiとgovmomiは、内部では同じAPIを叩いているのにプロパティ取得の方法がかなり異なります。
違いを調べた結果、pyVmomiではPythonのメタプログラミングが使われていました。
本記事ではこの仕組みについて紹介します。
govmomiとpyVmomiのプロパティ取得の実装の違い
vSphere Management APIでのプロパティの取得
vSphereでは、VirtualMachine等のオブジェクト(ManagedObject)の詳細なプロパティを取得したい場合 API RetrievePropertiesEx を使用します。
govmomiでの実装
govmomiでは以下のような実装となります。vmを取得した後、メソッドRetrieveOne (= RetrievePropertiesEx)でプロパティを取得しています。
ctx := context.Background()
u, _ := url.Parse("https://user:pass@127.0.0.1:8989/sdk") // vcsim(vCenterのシミュレータ)を使用
c, _ := govmomi.NewClient(ctx, u, true)
// VMを取得
finder := find.NewFinder(c.Client)
vm, _ := finder.VirtualMachine(ctx, "DC0_H0_VM0")
// vmのプロパティconfigを取得
pc := property.DefaultCollector(c.Client) // プロパティコレクターを取得
var moVM mo.VirtualMachine // プロパティを受け取るための変数
props := []string{"config"} // mo中の取得したい属性(フィールド)のパス
_ = pc.RetrieveOne(ctx, vm.Reference(), props, &moVM) // propsに対応する属性を取得し、結果を破壊的にmoVMに書き込み
fmt.Println(moVM.Config.Version) // vmx-13
pyVmomiでの実装
一方、pyVmomiでは以下のように取得できます。
si = connect.SmartConnect(host='localhost', port=8989, user='user', pwd='pass') // vcsim(vCenterのシミュレータ)を使用
# VMを取得
search_index = si.content.searchIndex
vm = search_index.FindByUuid(uuid="b4689bed-97f0-5bcd-8a4c-07477cc8f06f", vmSearch=True, instanceUuid=True)
# vmのプロパティconfigを取得
print(vm.config.version) // 'vmx-13'
vmを取得したら、そのまま config の属性を参照することができました。
RetrievePropertiesExを呼んでいる箇所が見当たりません。APIリクエストしていないのでしょうか?
いえ、APIリクエストは確かに発生しています。
例えばvCenterとの接続を切ってから属性を参照すると、以下のようにConnection refusedのエラーが発生します。
>>> vm.config.version
Traceback (most recent call last):
...
ConnectionRefusedError: [Errno 111] Connection refused
いったいどのタイミングでリクエストしているのでしょうか?
仕組み
結論から言うと、属性参照の際にAPIが呼ばれていました。
内部でPythonの機能「プロパティ」を使って、属性参照を動的に処理しています。
要素をプロパティとして定義
まず、ManagedObject のインスタンスが作成される際、その各要素をプロパティとして作成します1。
# getter関数を生成(後述)
getter = Curry(ManagedObject._InvokeAccessor, info)
# 要素を作成
# 例:configの場合 dic["config"] = property(getter)
dic[info.name] = property(getter)
property はプロパティを作成する組み込み関数です。
プロパティを定義すると、属性の参照時に代わりにプロパティの getter を呼び出すことができます。
vm.info # 実際には関数呼び出し getter() と同じ
こうすることで、属性参照と同じ構文でメソッドを呼び出すことができるようになりました。
プロパティのgetterでAPIリクエストを行う
続いてgetterの中を見ていきます。
ManagedObject._InvokeAccessor は内部で SessionOrientedStub#InvokeAccessor を呼び出しています。このメソッドがAPIリクエストを担っています。
- https://github.com/vmware/pyvmomi/blob/e6cc09f32593d263b9ea0b611596a2c505786c6b/pyVmomi/VmomiSupport.py#L384
- https://github.com/vmware/pyvmomi/blob/e6cc09f32593d263b9ea0b611596a2c505786c6b/pyVmomi/SoapAdapter.py#L1731
class SessionOrientedStub(StubAdapterBase):
def InvokeAccessor(self, mo, info):
retriesLeft = self.retryCount
while True:
try:
if self.state == self.STATE_UNAUTHENTICATED:
self._CallLoginMethod()
# メソッド呼び出し
obj = StubAdapterBase.InvokeAccessor(self, mo, info)
# ...
Curryで遅延評価をする
これで属性参照の裏側でAPIリクエストができるようになりました。
最後に型合わせとして Curry が使われています。
getter として欲しいのは以下の無引数関数で、ManagedObject._InvokeAccessor(info) を実行します。
def getter():
return ManagedObject._InvokeAccessor(info)
上記と同じことを、ここでは Curry クラスを使用してより簡潔に実装しています。
getter = Curry(ManagedObject._InvokeAccessor, info)
実装は以下のようになっています。名前にもあるように、カリー化をしています。
class Curry(object):
def __init__(self, f, *args):
self.f = f
self.args = args
def __call__(self, *args, **kwargs):
args = self.args + args
return self.f(*args, **kwargs)
__call__ メソッドを定義することで関数と同じように呼び出し可能にしています。
このクラスによって、途中まで引数を適用した状態の関数を作ることができます。
def __call__(self, *args, **kwargs):
args = [info] + args
return ManagedObject._InvokeAccessor(*args, **kwargs)
こうして、 vm.config が ManagedObject._InvokeAccessor(info) に置き換えられAPIリクエストが実行されます。
おわりに
以上、pyVmomi上で使われている、プロパティによるメタプログラミングの紹介でした。
ここまで凝ったものが必要になる機会はあまり多くないかもしれませんが、書き味をシンプルにしたい場面で覚えておくと応用が効きそうです。
-
少しややこしいですが、厳密に書くと「(vSphere Management APIの用語での)『プロパティ』をManagedObjectクラスの属性として扱うために(Pythonの構文としての)『プロパティ』を使用している」ということです。 ↩