ぶていのログでぶログ

思い出したが吉日

Ruby向けのLINSTOR APIクライアントを作った

LINSTORはLINBIT社が出しているSoftware-Defined-Storageを実現するためのミドルウェアである。 もともとはdrbdadmの代替として登場したと記憶しているが、いつのまにかDRBD以外の管理もできたりk8sやOpenStackから利用できるようになっている。

linbit.com

ペパボのプライベートクラウドでは、OpenStack CinderのバックエンドとしてLINSTOR + DRBDを利用している。 LINSTORはとても良くできていてほとんど手間がかからないのだが、時たま障害などによりCinder上ではボリュームが消えている(存在しない)がLINSTOR上残っているということが起こる。 残っているボリュームを残しておくと、ストレージ容量を圧迫するので削除してしまいたい。 LINSTORにはlinstor-clientというコマンドが用意されているが、それだけでは削除漏れのボリュームかどうかわからないためCinder側のボリューム情報と突き合わせる必要がある。 そうなるとlinstor-clientだけではだめでopenstack-clientも必要になり少々手間だ。 そこでこのLINSTORボリュームの一覧とCinderボリュームの一覧を突き合わせて、不要なボリュームなら削除するということを一発でやりたい。 両方ともPythonのスクリプトなのでPythonを使って作ることも考えたが、Rubyで書くことに慣れていてかつYaoというOpenStack APIクライアントのRuby実装をメンテナンスしていることもあり、LINSTOR APIクライアントのRuby実装をつくることにした。

openapi-generator

LINSTOR APIはなんとSwaggerで公開されている。

app.swaggerhub.com

ここを見てFaradayなりを使って実装していくのが王道だろう。 しかし、Swaggerで公開されているということはなんらかのフレームワークに則ってAPIが設計されているのではないかと考えた。 案の定OpenAPIを用いて設計されていて、しかもAPIを定義しているYAMLが公開されていることを見つけた。 https://github.com/LINBIT/linstor-server/blob/master/docs/rest_v1_openapi.yaml

OpenAPIを使っているのであればopenapi-generatorを使って機械的にクライアントライブラリを実装できることを知っていたので、これを使うことにした。

github.com

openapi-generatorにはCLIがあり、そしてDockerイメージが公開されているのでこれを使うとシュッと生成できる。 詳しいことは省略するので、気になる人は生成スクリプトを見てほしい。

github.com

ハマりどころ

機械的に生成できるといってもすんなりいかないこともある。 openapi-generatorがエラーを吐かなくても実際に使ってみるとエラーになる…ということが稀に起こる。 以下ではLINSTOR APIクライアントを作るときにハマったことをメモしておく。

ノードタイプの定義は先頭のみ大文字ではなく全部大文字だった

LINSTORノードの一覧を返すAPIがあり、その中にノードの種類を示す情報が含まれている。 openapi.yamlでの定義では↓のとおりになっている(コードはここ)。

        type:
          type: string
          example: Satellite
          enum:
            - Controller
            - Satellite
            - Combined
            - Auxiliary
            - Openflex_Target

enum配下に列挙されいてる文字列が期待される情報なのだが、実際にlinstor-clientを使ってAPIを叩いてみるとすべて大文字になっていることがわかる(NodeTypeの部分)。

$ linstor node list
+---------------------------------------------------------------+
| Node          | NodeType  | Addresses                | State  |
|===============================================================|
| test-server01 | SATELLITE | 192.168.0.1:3366 (PLAIN) | Online |
| test-server02 | SATELLITE | 192.168.0.2:3366 (PLAIN) | Online |
-- snip --

どうも自動生成されたライブラリでは、APIが返してくる値をValidateしているらしく文字列が不一致だとraiseしてしまう。 幸いにもこの不一致部分はopenapi.yamlではここだけだったので、openapi-generatorに渡す前にsedで大文字化することで対処した。

oneOfに対応したクラスが生成されない

正しくOpenAPIを理解していないのだが、OpenAPIにはoneOfというキーワードがある。 oneOfに指定されたどれかのサブスキーマを使うという宣言で、データの検証に用いられる。 詳しくはoneOf, anyOf, allOf, notを参照。

で、どうもopenapi-generatorのRubyテンプレートではこのoneOfに指定されたクラスが生成されないようだ。 例えば↓のような定義の場合は、OneOfDrbdVolumeDefinitionみたいな検証クラスが必要になる。

          oneOf:
            - $ref: '#/components/schemas/DrbdVolumeDefinition'

このOneOf〜〜クラスは、親クラス(今回の場合 VolumeDefinitionLayer)からデシリアライズされるときにbuildメソッドもしくはbuild_from_hashメソッドを呼び出されるようだった。 呼び出し時にデシリアライズしたいデータのハッシュが来るので、その中身を見て正しいクラスにラップして返すみたいなのを期待しているようだ。 そのため、例のようなOneOfDrbdVolumeDefinitionは以下の実装になった。

module LinstorClient
  class OneOfDrbdVolumeDefinition
    def self.build_from_hash(attributes)
      DrbdVolumeDefinition.new.build_from_hash(attributes)
    end
  end
end

実は、入力をそのままHashにして返しても動作するのだが、一応ちゃんとクラスにラップした。 生成するたびに、このクラスの差し込みとrequireを追加するのが面倒なので、openapi-generatorのRubyテンプレートをコピーして独自でテンプレートを定義することにした。

APIホストが変更できない?

生成されたライブラリにはConfigurationクラスが含まれている。 デフォルトではlocalhostを参照するようになっているが、別のホストを使うことのほうが多いと思う。 例えば以下のようにすると変更できるはずである。

LinstorClient.configure do |c|
  c.host = "203.0.113.25:3370"
end

しかしながらこれはうまく行かない。 私が使い方を正しく理解していない可能性もあるが、どうやらHTTPメソッドごとに接続先のサーバを変更する機能があるようだが、Rubyテンプレートの場合server_indexが0で初期化されているために、常にserver_operation_indexを参照しようとしてしまう。 そして、server_operation_indexは初期化されていないのでlocalhostにフォールバックしてしまうということが起こってしまう。

github.com

クライアント利用時にserver_operation_indexを初期化するなどの方法も考えたが、利用するときに煩雑になってしまうので前述した独自テンプレートでnilにするようにパッチを当てている

おわりに

OpenAPIとopenapi-generatorを使ってRubyのクライアントライブラリを機械的に生成することができた。 ハマりどころはあるが自動で生成できるのは便利だと感じた。 生成したライブラリは実際に運用でも使い始めていて、うまく動いていると思う。 私と同じようにRubyでLINSTORを操作したくなったら、RubyGemsにあげているので利用することをおすすめする。

rubygems.org