彷徨えるフジワラ

年がら年中さまよってます

Python library for ZFS (pyzfs) のバグを修正する

本エントリは Solaris Advent Calendar 2016 の 22 日目です。

Python library for ZFS

libzfs の zfs_iter_*() 系 API 利用について、ソースツリーを網羅的に調べた際に、Python から ZFS を利用するためのライブラリが提供されているのに気付きました (ソースツリー上の格納先から、以下このライブラリを pyzfs と呼びます)。

OpenIndiana 環境であれば、/usr/lib/python2.6/vendor-packages 配下に zfs パッケージとしてインストールされており、システムデフォルトの Python 処理系から事前準備等無しで使用できる筈です。

しかし実際に使ってみると幾つか不具合があったので、これらを修正してみました。

修正版ソースは Bitbucket 上で公開しています。本ブログエントリに関するコードは pyzfs ディレクトリ配下にあります。

illumos 本家に正式に修正要求出さないとなぁ……

要素一覧取得が機能しない

pyzfs における ZFS 要素一覧取得を実際に使ってみると、正しい結果を得られないケースの方が多いです。

例えば、以下のような稼働環境で:

$ zfs list -d 1 -o name -H rpool
rpool
rpool/ROOT
rpool/dump
rpool/export
rpool/swap
$

pyzfs による "rpool" 直下の非スナップショット要素の一覧取得を行っても、空の結果とかだったりします。

$ python -i
Python 2.6.4 (r264:75706, Nov 16 2013, 20:49:19) [C] on sunos5
Type "help", "copyright", "credits" or "license" for more information.
>>> import zfs.ioctl
>>> def iterfilesystems(basename):
...     cookie = 0
...     while True:
...         result = zfs.ioctl.next_dataset(basename, 0, cookie)
...         (element, cookie, props) = result
...         yield element
...
>>> list(iterfilesystems('rpool'))
[]
>>>

あるいは、存在するスナップショットに対して:

$ zfs list -d 1 -t snapshot -o name -H rpool/ROOT/BE20161216-01
rpool/ROOT/BE20161216-01@install
rpool/ROOT/BE20161216-01@2012-03-29-04:34:40
rpool/ROOT/BE20161216-01@2016-12-10-06:50:18
rpool/ROOT/BE20161216-01@2016-12-15-07:49:00
$

pyzfs だと一部しか認識されない、といった感じです。

>>> def itersnapshots(zfsname):
...     cookie = 0
...     while True:
...         result = zfs.ioctl.next_dataset(zfsname, 1, cookie)
...         (element, cookie, props) = result
...         yield element
>>>
>>> print '\n'.join(itersnapshots('rpool/ROOT/BE20161216-01'))
rpool/ROOT/BE20161216-01@2012-03-29-04:34:40
rpool/ROOT/BE20161216-01@2016-12-15-07:49:00
>>>

zfs.ioctl モジュールは pyzfs 中唯一の C 実装なので、以下のような DTrace スクリプトを使って ioctl() の出入りを見張ってみました。

fbt:zfs:zfs_ioc_snapshot_list_next:entry,
fbt:zfs:zfs_ioc_dataset_list_next:entry
{
    self->zc = (zfs_cmd_t*)(args[0]);
    printf("%s entry\n", probefunc);
    printf("  zc_name            = %s\n", self->zc->zc_name);
    printf("  zc_nvlist_dst_size = %llx\n", self->zc->zc_nvlist_dst_size);
    printf("  zc_cookie          = %llx\n", self->zc->zc_cookie);
}

fbt:zfs:zfs_ioc_snapshot_list_next:return,
fbt:zfs:zfs_ioc_dataset_list_next:return
{
    printf("%s return = %d\n", probefunc, args[1]);
    printf("  zc_name            = %s\n", self->zc->zc_name);
    printf("  zc_nvlist_dst_size = %llx\n", self->zc->zc_nvlist_dst_size);
    printf("  zc_cookie          = %llx\n", self->zc->zc_cookie);
}

「rpool 配下は空」となる場合の ioctl() の出入りを見張ってみると:

$ pfexec dtrace -s trace_zfs_ioctl.d -c "python pyzfs/sample/py_get_filesystems.py rpool" -q
zfs_ioc_dataset_list_next entry
  zc_name            = rpool
  zc_nvlist_dst_size = 800
  zc_cookie          = 0
zfs_ioc_dataset_list_next return = 12 ※ ENOMEM
  zc_name            = rpool/ROOT (*1)
  zc_nvlist_dst_size = b08
  zc_cookie          = 17478891 (*1)
zfs_ioc_dataset_list_next entry
  zc_name            = rpool/ROOT (*2)
  zc_nvlist_dst_size = b08
  zc_cookie          = 17478891 (*2)
zfs_ioc_dataset_list_next return = 12 ※ ENOMEM
  zc_name            = rpool/ROOT/BE20161216-01 (*3)
  zc_nvlist_dst_size = c90
  zc_cookie          = 1ba17752
zfs_ioc_dataset_list_next entry
  zc_name            = rpool/ROOT/BE20161216-01
  zc_nvlist_dst_size = c90
  zc_cookie          = 1ba17752
zfs_ioc_dataset_list_next return = 3 ※ ESRCH ⇒ 走査打ち切り通知
  zc_name            = rpool/ROOT/BE20161216-01/
  zc_nvlist_dst_size = c90
  zc_cookie          = 1ba17752
$

要するに:

  • (*1) ENOMEM 終了でも、zc_name や zc_cookie フィールドは改変済みなのに、
  • (*2) ENOMEM 時の再試行で、フィールド値が改変されたままなので、
  • (*3) ENOMEM 後の再実行での返却値が、想定外のものになっている

という状況な模様です。

zfs.ioctl モジュールの C 実装を見ると、確かに ENOME 時の zc_name や zc_cookie 復旧処理が抜けていました。libzfs の zfs_iter_*() 系 API の ioctl() 呼び出しを行う内部実装 zfs_do_list_ioctl() では、この辺の復旧処理をキチンとやっています。

復旧処理の抜けを修正することで、zfs.ioctl.next_dataset() が正しく機能するようになります。

Bitbucket リポジトリ中には、各ユーザの権限範囲内でローカルに修正版ライブラリをビルドしやすいように、setup.py を置いてありますので、pyzfs/README.rst での説明を参照してビルドしてください。

なお、ioctl.c のコンパイルには、ソースツリーでしか提供されない内部 C ヘッダが必要なので、illumos-gate 自体の入手は必要です (諸々のビルド前事前準備等の作業は不要)。

また、ビルドに使用する illumos-gate ソースツリーと、実稼働環境との間で、各種定義の値が食い違うと、実行時に segment fault 等が発生します。

定義の食い違いを確認するには、上記 README.rst 中の "Trouble shooting" での説明に従い、pyzfs/checktools で make を実行してみてください。なお、各種定義の食い違いが検出されても make の実行自体は成功しますので、実行結果出力の評価は、各自で行う必要があります。

Dataset.__repr__() で AttributeError が発生

pyzfs では、ファイルシステムやスナップショットを抽象化するクラスとして、zfs.dataset.Dataset クラスが提供されています。

しかし、このクラスのインスタンスを文字列化しようとすると、想定外の AttributeError が発生してしまいます。

$ python -i
Python 2.6.4 (r264:75706, Nov 16 2013, 20:49:19) [C] on sunos5
Type "help", "copyright", "credits" or "license" for more information.
>>> import zfs.dataset
>>> rpool = zfs.dataset.Dataset('rpool')
>>> rpool
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/lib/python2.6/vendor-packages/zfs/util.py", line 51, in default_repr
    str += " %s: %r" % (v, getattr(self, v))
AttributeError: 'Dataset' object has no attribute '__props'
>>>

この問題は、実は ZFS とは全然無関係で、以下の組み合わせによって発生しているものです。

  • "__foo" 形式の属性名は Python が自動的に "_classname__foo" に改名する
  • 文字列化処理はとにかく "__foo" という属性名を参照しようとする

文字列化の際に、"__" で始まる属性は無視することで、Dataset クラスの文字列化が、正しく機能するようになります。