Chienomi

SSHFS (ro) + OverlayFS + Nemoでのファイル操作

Live With Linux::technique

本記事はReadyNASのトラブルと、その中身に踏み込むで言及した、HDDストレージをroot所有にした上でread onlyでマウントする運用を前提にしている。

この場合、

  • ファイルの所有はroot:rootとなり、実際に書けるかどうかに関係なくNemoは書き込みを行わない
  • SSHFSは他のユーザーから(rootでも)見えない
  • ユーザーがマウントしたSSHFSと通常ディレクトリのOverlayFSを作ると、ユーザー、rootともにアクセスできない

といった壁があり、そのためすんなりとはうまくいかない。

やりたいこと

HDD(NAS)に対するファイル移動をバルクで行うため(シーケンシャルに書き込むため)に、rsyncで転送可能なファイルツリーを構成したい。それにあたって、Nemoでドラッグアンドドロップで作業するため、あたかもNAS上のツリーに対して操作を行っているような形で操作し、それを反映する形をとりたい。

このときリネームは制限されるのは良しとして、GUIによる操作でファイルの整理をしたい。

OverlayFSとは

概要

統合ファイルシステムはStackableファイルシステムの一種で、unionマウントやunionファイルシステムとも呼ばれる。 unionファイルシステムという名前はUnionFSと間違えやすくあまり適切でないが、簡単に言うと複数のファイルシステム(あるいはディレクトリ)をひとつのディレクトリに混ぜてマウントするものである。

典型的な利用としてはライブCDでの利用がある。 CD上のファイルシステムは必然的にroであり、書き込むことができない。しかし全ての差分をメモリに置いておくと使うに従ってメモリを圧迫していくため、「persistentを持つライブCD」を実現するために差分を他のディスクに書き込む必要がある。

この場合に、読み取り専用のファイルシステムの上に書き込みできるファイルシステムを重ね、書き込みは書き込みできるファイルシステムに残すようにすれば良いわけだ。このような用途にunionマウントが活躍する。

しかし、そういう説明だとかなり限定的な利用しかできないように見える。 実際は、「複数のディレクトリ上のファイルがひとつのディレクトリにあるように見せかける」ことができるため、活用範囲は広い。

読み取り専用で重ねる

こちらの使い方が本命だと思う。 マウントしたOverlayFSをroにして、複数のディレクトリを統合する。

sudo mount -t overlay overlay -o lowerdir=$HOME/DirA:$HOME/DirB ~/mnt

これにより~/DirA~/DirBの両方の中身を持つ~/mntが出来上がる。 両者でファイルパスが競合した場合、先に指定した~/DirAのものが優先される。

この場合、~/mntへの書き込みはできない。

そう言うと実用性に乏しいように思えるかもしれないが、そもそも~/DirA~/DirB自体は書き込めるのであれば、これらに変更を加えればそのまま~/mntにも反映される。

この使い方はとてもシンプルだ。複数のディレクトリコンテンツを持つひとつのディレクトリが作れる。ディレクトリコンテンツの操作はもととなるディレクトリ上で行う。

これは特にファイル管理の都合とファイル閲覧の都合が異なる場合に有効だ。 例えばあるアプリケーションがオーディオファイルをすべて$ARTIST/$ALBUM/$TRACK.mp3の形で提供されることを求めるが、ユーザーとしては音声データは提供元(例えばAmazonなのかiTunesなのか)によって分けて管理したいと考えているとしよう。単純に考えられるのはリンクファームを作ることで異なる構造を持つディレクトリを作ることだが、大量のリンクを作る必要があり、また都度更新も必要となる。 OverlayFSを使うことでこの問題を解消可能だ。

また、ひとつのディスクに収めるには巨大すぎるライブラリを複数ディスクにまたがってひとつのヒエラルキーで構築することもできる。 例えば3TBのビデオライブラリがあるとしよう。これらは$CATEGORY/$ARTIST/TITLE.mp4のような形のヒエラルキーを構成するのだが、これを収めるにはそれだけで3TB以上のディスクが必要になる。もちろん、大きなディスクを用意したり、あるいはBtrfsやLVMやmdを使って複数ディスクを束ねたりしても良いのだが、OverlayFSを使えば複数のディスクに分けて管理することができる。 この場合、ファイルの所在はバラバラだが、個々のディスクが独立して機能するのも魅力的だ。

書き込めるように統合する

ライブCDで使うのと同じような状態にする。

sudo mount -t overlay overlay -o lowerdir=$HOME/DirA,upperdir=$HOME/DirB,workdir=$HOME/union_worker ~/mnt

先程と同様に~/DirA~/DirBの両方を持つ~/mntがマウントされるが、今度は書き込みを行うことができる。書き込みを行った場合、~/DirBのほうに変更が加えられる。

~/DirAにあるファイルを変更した場合、そのファイルの(変更された)完全なデータが~/DirBに置かれる。 ファイル名を変更した場合も同じ扱いである。

先程と違い、指定の後先ではなく、upperdirのほうが上に載っている状態なので、パスが競合した場合~/DirBが優先される。 ~/DirAにあるファイルを削除すると~/DirBにはキャラクタデバイスが残る。

workdirはそのまま、作業領域であり、upperdirと同じファイルシステム上にある必要がある。 なんのためにあるのかは、正直よくわからない。消しても支障がないように見える。

あとあと統合するために作業領域としてひとつのディレクトリにまとめたいという場合は、大量のキャラクタデバイスが残ってしまう可能性があり、またリネームするたびに書き込めるほうのディレクトリに複製することになるため、あまりおすすめできる使い方ではない。 見た目上合わせるためにroでマウントし、実際の操作は実ファイルシステム上で行うほうが良い。

比較的使いやすい方法として、並列でデータのダウンロードを行うような場合において、それらがすでにダウンロードしたデータを必要とする場合、ダウンロードするためにすべてのデータが置かれるべきだが、その容量が大きければHDDに配置したいと考えられる。だが、HDDに配置すると並列ダウンロードによって激しい断片化を引き起こす。

そこで、HDD上のデータをOverlayFSの下層にし、SSD上のディレクトリを上層にするという方法がある。この場合、ダウンロードされるのは(断片化の影響の少ない)SSD上で、あとからHDDへ転送することが可能だ。

bindマウントとの関係

特定のディレクトリ以下に他のディレクトリのデータを配置したい場合はbindマウントが有効であり、適している。

OverlayFSの場合は、同一ディレクトリ上に複数ディレクトリに存在するファイルを「混ぜて」配置できるのが特徴的。

もし、あるディレクトリにA, B, C, D, E, Fと6つのディレクトリがあり、A, B, CとD, E, Fがそれぞれ別ディスクにある場合に、3つのディレクトリに対しbindマウントを行うか、ひとつのOverlayFSにするかは好みによるところ。 場合によっては単純にシンボリックリンクでいいこともあるかもしれない。

問題を知る

まず、Nemoの問題は、Nemoは実際にファイルを書き込んで失敗したときに失敗を返すのではなく、自分でパーミッションをチェックしてできないものはやらない、という姿勢である、ということにある。 これが問題なのは見かけ上のユーザーとパーミッションが異なるときだ。例えばsshfs root@nas:. ~/mntのようにマウントしたとする。すると~/mntのファイルはNAS上でroot:root 755であれば、システム的にもroot:root 755に見えるし、今のユーザー(仮にharuka:haruka)では書けないわけだが、実際に実行すると~/mnt以下への書き込みはリモートホストではrootユーザーとして実行されるために書くことができる。 このような場合でもNemoは書き込みを行わない。

また、ターゲットが読み込み専用ファイルシステムで書けない場合はNemoは実際に書き込みを試してから知ることになるが、パーミッションに関してはこのような事前チェックするため、OverlayFSで書けないディレクトリに対して書けるディレクトリを上に乗せている場合もNemoは書かない。

私は他のファイルマネージャを試していないので、そもそも「Nemoを使わない」ことでこの問題は解消されるかもしれないし、その可能性は低くない。

OverlayFSとSSHFSとの間で問題が生じるのは、SSHFSがユーザーレベルのFUSEであるのに対し、OverlayFSはカーネル組み込みのファイルシステムであることだ。 つまり、OverlayFSは常にrootとして動作するが、SSHFSはマウントしたユーザーにしかアクセスできない。そのため、ユーザーがマウントしたSSHFSとOverlayFSを組み合わせるとrootからもユーザーからもアクセスできない状態になる。

この問題は、rootがマウントしたSSHFSに対しては問題が生じない。OverlayFSはrootとして動作し、その下層にあるSSHFSに対しても動作するからだ。

そもそもroot所有ではなくroだけにする

この問題は「複合だからややこしい」のであり、リモートのファイルシステムがユーザーから書けるように見えるのであれば単にrootでSSHFSをマウントし、OverlayFSを構築すればうまくいく。

「root所有にしてユーザーがパーミッションで書けないようにする」と「読み込み専用ファイルシステムとしてマウントする」はどちらもHDDに対して不用意な書き込みを発生させないための方法だが、方法が二重になっている。 パーミッションによる設定をすればアプリ側で書き込みを断念する(Nemoもそうだが)ケースがあるためそれはそれで有用なのだが、なくても実害はない。

問題を解決する

「実際のパーミッション」と「見かけ上のパーミッション」が違うことをうまく利用すれば問題を解決できる。

まず、mountコマンドの実行はそれ自体が権限チェックを行う。このため、「root以外でmountを実行できない」仕様であり、システムコールが失敗するかどうかは関係がない。

その上で、OverlayFSはLinux 5.11からユーザーでもマウントできるようになっている。つまり、mount(8)が一般ユーザーの実行を拒否しなければユーザーがマウントしたSSHFSに対してOverlayFSを載せることはできるのである。

それを満たすのは「実際はrootではないが、一見rootに見える」という形にすることである。

なので、次のようにして見かけ上のrootを獲得する。

% unshare --user --map-root-user --mount

これは一般ユーザーで実行できる。するとプロセス内ではrootに見えるが、システム的にはユーザーのものであるプロセスが出来上がる。

マウントポイントは共有しない(共有するとうまくいかない)。 そこでこの状態でSSHFSマウントを行う。ユーザーの鍵は読むことができるが、プロセス的にはrootに見えているので/root/.sshを読みに行ってしまう。しかし実際にはこのプロセスはユーザーのプロセスなのでアクセスに失敗する。 この都合で、~/.ssh/configは読めないのでちゃんとフルで指定する必要がある。

# sshfs foo@192.168.1.10:. /home/foo/mnt -o IdentityFile=/home/foo/.ssh/nas_ed25519

ちなみに、$HOMEは維持されているため、~/.ssh/configが読まれないだけで、~$HOMEは普通に使える。

このプロセスは見かけ上rootなので、mountのチェックをパスできる。このため、OverlayFSをユーザーとして実行できる。 upperdirworkdirは同一ファイルシステム上に置かなければならないことを忘れずに。

# mount -t overlay -o lowerdir=/home/foo/mnt,upperdir=/home/foo/classify/tree,workdir=/home/foo/classify/work /home/foo/classify/merge

しかしマウントポイントが独立しているため、普通にこの状態で/home/foo/classify/mergeを見に行くと空ディレクトリに見える。そのため、unshareしたプロセスからNemoを起動する必要がある。

このように起動したNemoは見かけ上のユーザーがrootであるため、Nemoには「権限昇格」と表示される。 ただし、実際にはrootではないため、システム上のroot所有のファイルに対して書けるわけではない。

だがこの状態のNemoで書き込めば権限チェックをパスするため、見かけ上root所有だが実際は書けるディレクトリに対して書くことができる。Nemoはこうした異なる環境間のNemo同士でファイルを受け渡すことができるため、特定ウィンドウでしかできないという問題はあるが、Nemoで作業できる。

なお、cgroupsを使わずにrootとしてNemoを起動する前提であれば、rootでSSHFSをマウントし、OverlayFSをマウントしてNemoを起動するという手順で同じようなことができる。

2022-03-28 追記

この運用を少しやってみたのだが、最終的には「不便すぎる」という理由で使うのをやめた。

問題は2点。

  • リネームできないのはやはり不便。Nemoでフォルダを作ることもできない
  • ファイルを移動したときに、「異なるファイルシステムへの移動」とみなされ、ファイルはコピーされてしまう

この問題から、結局必要に応じてファイルツリーを構築し、操作する形に変更した。

ターゲットディレクトリを開いた状態で(パス表示していればNemoで良い)パスをコピーし、mkdir -pに渡せば一気に作れるため意外としんどくない。

同一階層下にターゲットディレクトリがたくさんある場合は、仮にベースファイルシステムが/mnt、マージ用仮想ツリーが~/Mergeにあり、foo/bar/baz以下のツリーが欲しいとすると

% cd ~/Merge
% mkdir -p 'foo/bar/baz'
% cd /mnt/foo/bar/baz
% find . -type d | (cd ~/Merge/foo/bar/baz; while read; do mkdir "$REPLY"; done)

という感じですればまとめて作れる。 パス入力が面倒な場合

find . -type d > ~/tmp/dirlist

のようにリストを保存して、マージ用ディレクトリで端末を開き

mkdir $(cat ~/tmp/dirlist)

のようにして作るほうが楽。 (ディレクトリ名によってはreadを使うほうが安全。)