2014年4月23日

Raspberry PiをQEMUエミュレータ上で走らせる(2) 立ち上げ、X Window、ネットワーク

前回でQUEMが動き、エミュレータ上でカーネルとシェルが動き、基本的な動作が確認できた。次は、これを開発に必要な実用レベルに持って行く。

QEMU版への移行

まず、RPiには標準ではバッテリバックアップ付きのリアルタイムクロックはなく、この段階ではNTPDはおろかネットワークも動いていないので、時計が例の1970年元旦になっている。このままでも致命的ではないが、以下に述べるファイル修正の履歴(タイムスタンプ)が後日奇妙に見えるので、dateコマンドでそれらしい時刻を設定する。

root@rpi:/# date 04221250

次に、ホスト名がディスクイメージのコピー元と同じだと紛らわしいのでエディタで変えておく。エディタはディスクイメージにインストールされているものなら何でも使えるが、viが多少不安定な挙動を示したので、最悪nanoで。

root@rpi:/# cat /etc/hostname
rpi-qemu

余計なライブラリのプリロードを禁止。

root@rpi:/# cat /etc/ld.so.preload
#/usr/lib/arm-linux-gnuebihf/libcofi_rpi.so

最後に、必ずしも必要ではないが、実機とディスクデバイスの名前を揃えるためのudevルールファイルを作る。

root@rpi:/# cat /etc/udev/rules.d/90-qemu.rules
KERNEL=="sda", SYMLINK+="mmcblk0"
KERNEL=="sda?", SYMLINK+="mmcblk0p%n"
KERNEL=="sda2", SYMLINK+="root"

で、一度システムをシャットダウン。

root@rpi:/# halt

ホスト側のQEMUを^Cで殺し、もう一度本チャン起動してシステムをフル稼働にする。

h@spice:~/projects/rip$ qemu-system-arm -kernel kernel-qemu -cpu arm1176 -m 256 -M versatilepb -no-reboot -serial stdio -append "root=/dev/sda2 panic=1 rootfstype=ext4 rw" -hda rpi.img -redir tcp:2222::22

パラメタについていくつか…
・-m 256 システムメモリは256MBがハードコードされているのでこれより大きい値は指定するな、と言われている。
・-M versatilepb QEMU上の仮想プラットフォーム。実機ではBCM2708。
・-hda rpi.img 最初の立ち上げに使ったのと同じ自前のディスクイメージファイル。
・-redir tcp:2222:22 ホストのTCPポート2222をQEMUターゲットのポート22にリディレクトする。SSH用(後述)。

Xウィンドウ


これはあっけなく動いた。実機からコピーしたディスクイメージを使っているので、実機とほぼ同じ。ただし、QEMUの問題と思われる以下の問題が発見された。
・マウスポインタがホストから見える「物理ポインタ」(四角い小さなドット)とターゲット(RPi)に見える論理ポインタ(矢印、Iビームなど)が乖離する。論理ポインタがQEMUのウィンドウの外には出られないクリッピングを起こすので両者を近づけることはできるが、そのうちまた離れてしまうので煩わしい。VNCクライアントなどでもこの兆候はあるが、あちらは自分でできるだけ一致させるように働くのに、こちらはその努力が見えない。
・ロカール(locale)が正しくないのか、ASCIIキーボード「@」や「|」のキーが違うコードを出力している模様。まるでJIS配列キーボードの感じ。
・上下の矢印キーが上手く認識されない(左右の矢印キーはOK)。スクリーンエディタは別のキーを使えるが、bashでリコールバッファが使えないのは痛い。

2番目と3番目のキーコードの問題はロカールとかXの設定ファイルをいじれば動きそうな感じもするが、小生はこの辺が不得手であり、また元々現在の時計プロジェクトでは入力UIは不要なので詮索しない。キーが正常に動作したとしても、マウスポインタの問題でこのUIを使っての開発は実際的ではない。

ネットワーク

これも考えるより産むが易し、と言うか、考えと違う形であっけなく動いた、と言うより動いていた。

以下のスタートアップファイルがあることを確認する。

root@rpi:/# cat /etc/network/interfaces
auto lo

iface lo inet loopback
iface eth0 inet dhcp

ここで実現されるネットワークは、ターゲット側は仮想イーサネット。ホスト側のQEMUのユーザコードがNAT付きラウタ、DHCPサーバ、DNSサーバを実現する閉じたLAN。ターゲットは常に10.0.2.15、ホスト側仮想ゲートウェイは10.0.2.2、仮想DNSサーバは10.0.2.3を与えられるようだ(ターゲットの/etc/resolv.confはNetwork Managerが自動的に書き換える)。ホスト側の仮想DNSサーバはキャッシュオンリーなのでホスト側では何も特別なことをする必要はない。また、DHCPのベースアドレスはQEMU起動時のオプションで設定できるようだが試していない。

このNAT付きラウタというトポロジーのため、QEMUホストがファイアウォールとなり、QEMUホスト以外の他のネットワークホストからはターゲットは見えない。また、ラウタは純粋にユーザコードで実現されているので、QEMUホストの他のプロセスから見えるネットワークデバイスなども存在せず、ターゲットに対するトリッキーなラウティングとかも不可能。

従って、QEMUで走るターゲットをサーバとして使うアプリケーションの開発には向いていない。

唯一の抜け穴は、QEMU起動時に指定した -redir tcp:2222::22 でリディレクトされたTCPポート。QEMUホストのポート2222にアクセスすればターゲットRPiのポート22にアクセスしたことになるから、QEMUホストが見えるところならどこからでもターゲットでSSHでアクセスできる。

この辺のことは、5年ほど前にAndroidタブレットの立ち上げの仕事(OSはまだDonutだった)をしていたときに使っていたはずなのに、すっかり忘れている。加齢とともに記憶の揮発性が高まってきたのか…。

逆に、ターゲットからはQEMUホストが見えるところならどこでもアクセスできる。ただし、アクセスされた方はアクセス元はQEMUホストに見えるので注意。例えば、ターゲットからQEMUホストにSSHでアクセスすると、SSH_CONNECTIONは127.0.0.1(localhost)からアクセスされたと主張する。うちのシステムはセキュリティのためにSSHのアクセス元でフィルタリングをしているの修正が必要だった。

もう一つ気がついたことは、QEMUのラウタはICMPを通していないようだ。例えば、ターゲットからwww.yahoo.comにpingすると、IPアドレスは解決するのにpingその物は到達できないという現象に2時間悩んだ。

なお、設定によってはターゲットのコンソールがQEMUを起動したセッション(シェルウィンドウ)にリディレクトされ、loginプロンプトが出ることがあるが、これはよほどのことがない限り使わない方が無難。^Cを押すとコンソールに食われるのではなく、QEMUプロセスを殺してしまう。

さて、これで実機とほぼ同等の動作をするエミュレータができたので開発がし易くなる。仕事が増えた。

なお、QEMUでエミュレートされるCPUはあまり速くない。/proc/cpuinfoには600 BogoMIPSが表示されるが、ターゲットのX上でマウスを振り回すだけでtopのCPU使用率は100%になってしまう。だからリアルタイム性とかの確認は当然のことながら実機ですべき。

2014年4月22日

Raspberry PiをQEMUエミュレータ上で走らせる(1) QEMUのインストールとカーネルの起動

Raspberry Piを使ったCaller ID+天気予報付きディジタル時計プロジェクトを加速することにした。

今回は小生の通常の「字ばっかり」のプロジェクトと違ってXウィンドウを使ったUI(表示のみ)なので、表示具合を確認しながら進めなければならないが、実機がないと開発できないというのはやりにくい。そこで、QEMUを使って実機なしでエミュレートすることを計画した。

RPi用のQEMUについては、こちらとこちらの記事を参考にしたが、(当然のことながら)記事のとおりに進めてもその通り動作しない。以下にカットアンドトライで試した結果を報告する。

QEMUのインストール

まずはQEMUのインストールから。ホストは我が家の何でも屋のFC13(x86 32bit 8GB)。他のディストロでは少々違いがあるかもしれないが、そこは工夫してください。
 
上記記事によると、ほとんどのディストロの標準パッケージのQEMUはRPiのCPU(arm1176)をサポートしておらず、また実際にサポートしていないことが確認できたので、パッケージのインストールは諦め、ソースから作ることにする。

まず、適当な作業用ディレクトリを作り、そこにQEMUのGITをクローンする。

h@spice:~/projects$ mkdir rpi
h@spice:~/projects$ cd rpi
h@spice:~/projects/rpi$ git git://git.qemu-project.org/qemu.git

あとは普通にconfigureとmakeを走らせる。上記記事ではPREFIXを「/usr」にしているが、標準パッケージとのガチンコを避けるためにデフォルトの「/usr/local」にしておく。「git submodule update --init dtc」は、configureがDTC(libfdt)が見つからないと文句をいい、「libfdt-devel」パッケージが見つからなかったので。

h@spice:~/projects/rpi$ cd qemu
h@spice:~/projects/rpi/qemu$ git submodule update --init dtc
h@spice:~/projects/rpi/qemu$ ./configure --target-list="arm-softmmu arm-linux-user"    \
            --enable-sdl    \
            --prefix=/usr/local
 
・・・

h@spice:~/projects/rpi/qemu$ make
・・・

h@spice:~/projects/rpi/qemu$ sudo make install

上記記事では、ここで「qemu-system-arm -cpu help」でRPiのCPU(arm1176)がサポートされているかどうか確認することになっているが、これが上手くいかない。代わりに「qemu-arm -cpu ?」は動いたので、これでよいことにする。

h@spice:~/projects/rpi/qemu$ qemu-system-arm -cpu help
No machine specified, and there is no default.
Use -machine help to list supported machines!

h@spice:~/projects/rpi/qemu$ qemu-arm -cpu ?  
Available CPUs:
  arm1026
  arm1136
  arm1136-r2
  arm1176
・・・

RPiのイメージファイル

次は、また適当な作業ディレクトリを作って、カーネルイメージとディスク(SDカード)イメージファイルを作る。
・ カーネルイメージファイル: 自前のカーネルはダメ。以下のように専用カーネルをダウンロードする。
・ディスクイメージファイル: 動作するディスクイメージ(ファイルシステムイメージではない)なら何でもよい。ここでは実機「rpi」からコピーしてみた(このコピーは時間がかかるので先行してバックグラウンドで実行するとよい)。

言い忘れたが、この作業ディレクトリはディスクイメージファイル(小生の実機は8GB)を 作るので、ホストに十分なディスク空き容量があることを確認しておく。

h@spice:~/projects/rpi$ mkdir rpi-emu
h@spice:~/projects/rpi$ cd rpi-emu
h@spice:~/projects/rpi/rpi-emu$ wget http://xecdesign.com/downloads/linux-qemu/kernel-qemu
h@spice:~/projects/rpi/rpi-emu$ ssh rpi if=/dev/mmcblk0 bs=16M >rpi.img

イメージファイルができたらQEMUを起動する。

h@spice:~/projects/rpi/rpi-emu$ qemu-system-arm -kernel kernel-qemu -cpu arm1176 -m 256 -M versatilepb -serial stdio -append "root=/dev/sda2 panic=1 rootfstype=ext4 rw init=/bin/bash" -hda rpi.img
audio: Could not init `oss' audio driver
Uncompressing Linux... done, booting the kernel.

お馴染みの「Uncompressing Linux...」が表示され、デスクトップにRPiのコンソールウィンドウが現れるので嬉しくなる。

ここでのキモは、カーネルコマンドラインの「init=/bin/bash」で、ユーザレベルの初期化を何もせずにいきなりbashを起動する。

もし何らかのエラーがありカーネルがパニックになると、上記のコマンドラインでは自動的にリブートを繰り返す。「-no-reboot」オプションを追加すると、リブートせずにQEMUが終了する。いずれにせよデバックがやり辛いので、パニックのメッセージがコンソールに表示されたらコンソールウィンドウの MENU>PAUSE でウィンドウを止める。残念ながらこのウィンドウはスクロールできないようなので(もしかしたらできるかもしれないがよく分からない)、PAUSEは上手くタイミングを計らねばならない。

下はわざとエラーを起こして見たところ(ディスクイメージファイルが完成しないうちにマウントさせようとした)。


順調に行けば、bashのプロンプトが出てきて「やった~!」となるが、世の中そう甘くはない。起動しただけの状態では、言わば丸裸で、/procさえマウントされていないから、色々なシステムユーティリティが動かない。取りあえずちょっとだけやってみる。


ダミーのネットワークインタフェースが見えるが、MACアドレスはわざとらしい「~00:12:34:56」。カーネルのバージョンは、実機の3.10.24+ #614より新しそう。

取りあえずカーネルは動き、シェルが動くようになった。次はここでネットワークとXウインドウを動かすが、それは次回に。

おっと、一言。QEMUのコンソールウィンドウは、クリックすると以降のマウスクリックやキーボードの入力をすべてトラップするので、意図せずにトラップされた時は、パニックらずに「Ctrl+Alt+G」で逃げ出す。

2014年4月17日

Raspberry Pi の X のスクリーンブランキングを禁止する

Raspberry Piのビデオ端子(コンポジットまたはHDMI)にXサーバを走らせることができる。R Piでグラフィクスを使い方を色々検討してみたが、汎用性(他のプラットフォームでシミュレーションができる)などを考えると一番楽そうなのは結局Xを使うことのようだ。

残念なことに、デフォルトではスクリーンブランキングが働き、表示開始数分後には真っ暗になってしまう。

ネットでいろいろブランキングに禁止方法を探したが、最終的にたどり着いたのはXサーバの起動時にオプションを与える方法。これは表示のみのアプリケーションなので、ついでにXカーソルも隠しておく。

# X -s 0 -dpms -nocursor

これを起動時に自動的に行うには、/etc/lightdm/lightdm.conf の以下の行を変更すればよいらしい(まだ試していない)。

[SeatDefaults] 
xserver-command=X -s 0 -dpms -nocursor

で、RPiとXで何をしようとしているかと言うと、テレビの足元に置く天気予報+Caller ID(日本の「ナンバーディスプレイ」に相当)付ディジタル時計を実現したいのだ。小生は時計の正確性については一種病的なこだわりがあるが、手頃な値段の置時計型電波時計の市販品はあまり多くない上に、バックライトなしのLCDまたはオンデマンドつまりボタンを押したときだけ発光するLCDしかなく、我が家のように割と薄暗い家族居間で2~3mの距離から見るテレビの足元に置くには役に立たない。

ちょっと考えるとAndroidタブレットでできそうなものだが、タップしかできないUIは「置時計」には使えないしスクリーンサイズも価格も大げさ過ぎる。Amazon.comを見ながらいろいろ考えた結果思い付いたのは、車載カメラ用のLCDモニタ。3.5~4.5インチサイズで$20前後と手頃感言うことなし。

実際に1台手に入れた。RPiのコンポジットビデオ端子に繋ぎ、実験用電源で12Vを与えると快調に動作する。LCDが大したことないので、コントラスト、特に斜めから見たときの背景が黒くないなどの問題はあるが十分実用になる。実験したところ、電源電圧を5Vまで落としても動作するのでUSB用から電源も取れそうだが、RPiのUSBは外部への電源供給があまり余裕がないので別電源にした方がよいと思う。と言うことで、USB電源2系統付きのACアダプタも注文した。

この手のLCDモニタは、なぜか480x234の物理ピクセルを16:9の画面に配置する矩形ピクセル仕様で、入力は640x480の論理ピクセルとややこしいが、どうせ実際の画面レイアウトは実験的に決めるしかない。

取り敢えず簡単に時計を表示するには、以下のコマンドを走らせる 。

# DISPLAY=:0 xclock -digital -update 1 -strftime '%H:%M' -face curier-116:bold -foreground green -background black &
# DISPLAY=:0 xclock -digital -geometry +500+55 -update 1 -strftime '%S' -face curier-72:bold -foreground green -background black &

こんな感じ。

フォントだとかいろいろな面倒はありそうだが、ImageMagickとかと組み合わせて、何となくスクリプトだけで目的を達成できそうな感じがしてきた。

2014年4月5日

va_list の ap は消費される。

前回のデーモンプロジェクトでは結局ログメッセージをsyslogとstderrの両方に出力するようにし、以下のようなwarnx(3)の包み込みを書いたのだがうまく行かない。


static void __warnx(const char *fmt, ...)
{

        va_list ap;

        va_start(ap, fmt);
        if (lopt)
                vsyslog(LOG_WARNING, fmt, ap);
        if (eopt)

                vwarnx(fmt, ap);
        va_end(ap);
}

loptとeoptの両方が真の時、vwarnx(3)の出力が乱れるのだ。

vsyslog(3)とvwarnx(3) を入れ替えると、必ず後に呼ばれる方が乱れることから、v付きの関数が呼ばれる度にapが消費されるのではないかと考え、以下のようにそれぞれのv付き関数にapを新しく確保するようにしたらうまく行った。


static void __warnx(const char *fmt, ...)
{

        va_list ap;

        va_start(ap, fmt);
        if (lopt)
                vsyslog(LOG_WARNING, fmt, ap);
 
        va_end(ap);
        va_start(ap, fmt);
        if (eopt)
                vwarnx(fmt, ap);
        va_end(ap);
}

このことはva_start(3)のマニュアルページには明記されていない。

va_copy(3)を使ってapを複製・保存(va_copy(aq, ap);)するという手もあるのかも知れないが、その際、マニュアルページにはva_copy(3)で複製されたaqはva_end(aq);で開放しなければならないと書いてあるが、複製元のapについては明記されていない(通常はva_start(3)で確保されたapは必ずva_end(3)で開放)。

深入りするのが面倒なので今回はこれでよしとする。

2014年4月4日

stderr(標準エラー)をsyslog にリディレクトする

カーネルからNETLINKで情報をデーモンプロセスに送るアプリケーションを書いている。

最初はメモリアロケーションだとかファイルのオープンだとかの「起こり得ない」エラーの処理はてきとーにスタブコードを書いておいた。機能が順調に動作してコードが洗練されてくるに連れ、エラーが起こってもメモリリークが起きないようにだとかの細々した配慮を施したが、エラーの報告は取りあえず warn(3)とかで簡単に済ませてきた。しかしプロジェクトが完成に近づいたところで、ロギングもちゃんと実現しなければならなくなった。

エラーが起こった場合メイルとか何かでユーザに知らせることも考えられるが、取りあえずはsyslogを使ってシステムのログファイルに記録すればよい、となった。
 
ところで、コードの開発中はプロセスはフォアグラウンドで動作させていたので、エラーメッセージはwarn(3)で標準エラーに書き出しているだけだったが、これをできるだけ(と言うより「できれば」)変更しないでsyslogに喰わせてやりたい。またトラブルシュート用にコマンドオプションで従来どおり標準出力にも書き出せるようにしたい。…ということを「簡単に」実現したい、というのが今回の課題。

stderr = popen("/usr/bin/logger -i -t mydaemon", "w");

のようにstderrをパイプでloggerプロセスにつないでやることもできるが、もしこのデーモンが死んだときにloggerプロセスがいつまでも残っているかも知れないなどの懸念がある。

ぐぐってみたら、あった、あった。ユーザ定義の入出力ハンドラを持つストリームをオープンしてstderrと置き換えるいう方法だ。

#include <stdio.h>
#include <err.h>

static bool dopt, lopt;
static FILE *stderr2;

static size_t logit(void *cookie, const char *buf, size_t len)
{
        if (stderr2)
                fwrite(buf, len, 1, stderr2);
        syslog(LOG_WARNING, "%.*s", (int)len, buf);
        return len;
}

static cookie_io_functions_t log_fns = {
        NULL, (cookie_write_function_t *)logit, NULL, NULL,
};

static void setup_logger(void)
{
        if (dopt) {
                int fd = dup(fileno(stderr));

                if (fd < 0)
                        err(1, "failed to dup stderr.");

                if (!(stderr2 = fdopen(fd, "w")))
                        err(1, "failed to fdopen stderr.");
        }

        fclose(stderr);

        stderr = fopencookie(NULL, "w", log_fns);
        setvbuf(stderr, NULL, _IOLBF, 0);
}


int main(int argc, char *argv[])
{
...
        if (lopt)
                setup_logger();
...
        warnx("This is output to stderr, which is redirected to logit().");

上が今回試してみて動作したコードの一部。

fprintf(3)、warn(3)など、凡そ標準エラーストリームにこのプロセス内で書き出すデータはすべてlogit()にリディレクトされるから、その後は如何様にでも処理できる。

キモはsetup_logger()から呼ばれるfopencookie(3)が、log_fnsにユーザの定義する読み込み、書き出し、シーク、クローズのハンドラを持つストリームをオープンすること。GLIBCではstdin、stdout、stderrはマクロではなく普通の変数だから、新しい値をいつでも代入でき、その後はfprintf(3)やwarn(3)などは新しいstderrに書き込むようになる。

もう一つのキモはstderrを更新した後に呼んでいるsetvbuf(3)。これがないとストリームバッファがいっぱいになるまで出力ハンドラは呼ばれないから、エラーメッセージのように何か起きたらリアルタイムで出力したい用途には好ましくない。ここではバッファ単位を行にしてうまく行っている。

こうして標準出力をまるごとsyslog(3)にリディレクトすることに成功したが、問題点があることも分かった。
  • リディレクションはストリームレベルなのでプロセス内で完結しており、例えば子プロセスの動作を買えることはできない
  • warn(3)はストリームに出力する前にメッセージにプログラム名をプリフィックスとして付加するが、syslog(3)もまた同様の処理をするのでプログラム名が二重に付加されて見苦しい
二重プリフィックスの問題はちょっと致命的だったので、せっかくリディレクションに成功したものの、この方法は諦め、warn(3)を__warn()という包み込み関数(またはマクロ)で置き換えてその中で場合分けして処理するという平凡な構成を最終的な実装とすることにした。

お粗末さまでした。

2014年2月4日

ルートファイルシステムはいじってはいけない

数年前から稼働している我が家のバックアップ専用サーバは2TBのHDDを6台使っている。パーティションは三つのRAIDデバイスで
  • md0 - RAID1 /boot
  • md1 - RAID5 / と /varを含むLVM
  • md2 - RAID6 /var/backup バックアップデータ専用
と言う構成。このバックアップ専用サーバ構築の前に使っていたHDD×4のRAID5(メインのファイルシステム)+HDD×2のJBOD(バックアップ専用)という構成のメインの単一の共通サーバのメインのRAID5のHDD2台が同時に死に、辛うじてバックアップ専用のJBODから数十年分の家族の写真ファイルなどを救出できたという経験からバックアップの重要性を思い知らされたので作った。一日24時間、週7日間我が家の全てのPCとサーバのデータをバックアップしている。

最初は、このサーバは4台のHDDで作成され、後にHDDを2台追加した。md0はHDD×4のRAID1からHDD×6のRAID1に、md2はHDD×4のRAID5からRAID6に拡張されたが、md1はHDD×4のRAID5+スペアHDD×2と言う構成のままだった。
 
本業の仕事でRAIDの内部を扱うことになり、何となく我が家のバックアップサーバを眺めて、md1がRAID5のままのことに気がつき、深く考えもせずに残りの2台を組み込んだRAID6に拡張したほうが良いような気がして作業を始めてしまったが、これが大きな誤りだったことに後で気がつく。今回はこの顛末記。

何が誤りだったかと言うと
  • 不注意: mdadmの--growオプションを--backup-file=無しで走らせた。
  • そもそもの誤り: ルートファイルシステムはみだりに変更してはいけない。
と言うこと。

まず、--backup-file=について。

RAIDの構成を変更する(RAIDのレベルを上げたりHDDを増やす)ときは、もしクリティカルセクション実行時に異常が起きても回復できるように--backup-file=を使って回復のためのデータを保持しておくべきなのだが、これを怠った。そしてマーフィの法則が示すように、構成変更作業中に停電が起きた。もちろんサーバはUPSを使っていたが、停電は数時間に及び、電源の回復前にシャットダウンしてしまった。ちなみに、当地はシリコンバレーと言う土地柄にも関わらず、日本では考えられないぐらい停電が多い。例年、3~4回の停電はざらだ。多くは嵐で立ち木が倒れて電線を切断するためだが、今回の停電は穏やかな日の夜明け前に起こり、原因は何だだったのか知らない。

サーバを立ち上げようとしたが、「ルートファイルシステムが見つからない」と言うエラーでフリーズしてしまう。冷や汗が流れてくるが、幸いなことに本機はバックアップサーバだから、1週間ぐらい死んでいても実用上問題はない。と言うことで、落ち着き、缶ビールを開けて修復作業を始める。

FedoraのインスタレーションCDを使ってレスキューモードで回復を図るが、何度やってもmd1のクリティカルデータが壊れているのでリシェープできないと言う。クリティカルセクションのバックアップファイルを作っておかなかった報いだ。

仕方がないので回復は諦め、md1のパーティションを作り直すことにする。幸いなことに、我が家にはもう一台バックアップ専用のアプライアンスがあり、こちらにこのバックアップサーバのバックアップデータ以外はすべて定期的にバックアップしてある(つまり我が家のシステムは別の部屋に設置した別のハードウェアに二重にバックアップしている)から、ファイルシステムを作り直してバックアップから回復すれば良いだろう。

と、希望的に考え、粛々と回復作業を行うが、一向に回復しない。ここで行った試行錯誤から、徐々に失敗の理由が明らかになってきた。すなわち、
ルートファイルシステムは他のファイルシステムと違い、ブートローダと密接にリンクしている
と言うことだ。

だから、単にファイルシステムを回復しただけではブートローダに認識されない。ルートファイルシステムそのもの(このサーバではルートファイルシステムを含むLVMが存在するRAID)とブートローダのポインタがちゃんと整合していなければならないのだ。

さて、実際の回復の道のりを思い出すだけ書いてみる。

対象のホストは「shadow」と言う名前で、問題のルートファイルシステム(オリジナル)は
  • LVM上の/dev/mapper/vg_shadow-LogVol00
  • LVMは4台のドライブからなるRAID5の/dev/md1上
  • RAID5の構成物理ドライブは
    /dev/sda2
    、/dev/sdb2、/dev/sdc2、/dev/sdd2(アクティブ)
    /dev/sde2
    、/dev/sdf2(スペア)
これをRAID6に変換しようと言うスケベ心を出したのが間違いだったので、元通りのRAID5を復活させる。 方針として、各種設定ファイルはオリジナルのままとして、RAIDやLVM、FSなどをオリジナルの設定ファイルに整合するように再構築する。

整合してなければならないことが確認できたのは以下の識別子。 

●/boot/grub/grub.conf内のブートメニュー項目のデバイス名とUUID。
shadow# cat /boot/grub/grub.conf
...
title Fedora (2.6.34.9-69.fc13.i686.PAE)
        root (hd5,0)
        kernel /vmlinuz-2.6.34.9-69.fc13.i686.PAE ro root=/dev/mapper/vg_shadow-LogVol00 rd_MD_UUID=cc8c918f:5b720e12:5c2a14a8:2e918d19 rd_LVM_LV=vg_shadow/LogVol00 rd_NO_LUKS rd_NO_DM LANG=en_US.UTF-8 SYSFONT=latarcyrheb-sun16 KEYTABLE=us rhgb quiet
        initrd /initramfs-2.6.34.9-69.fc13.i686.PAE.img

...


●LVMを構築する時に使う/etc/lvm/archive/vg_shadow_00000.vgの物理ボリューム(PV)のUUID。
shadow# cat /etc/lvm/archive/vg_shadow_00000.vg
...
         physical_volumes {

                pv0 {
                        id = "rLojRP-Ms7b-VKlA-p7Cj-n1nL-0MAy-nLy4Ty"
                        device = "/dev/md1"     # Hint only

...

●実際に構築されたRAIDデバイスのRAIDレベル、メタデータバージョン、ディスク台数、UUID。
shadow# mdadm -Esv
ARRAY /dev/md/0 level=raid1 metadata=1.0 num-devices=6 UUID=ec0da04a:e6ae42d2:bc3eb03c:7373ea18 name=localhost.localdomain:0
   devices=/dev/sdd1,/dev/sdb1,/dev/sdf1,/dev/sde1,/dev/sdc1,/dev/sda1
ARRAY /dev/md/1 level=raid5 metadata=1.1 num-devices=4 UUID=cc8c918f:5b720e12:5c2a14a8:2e918d19 name=localhost.localdomain:1
   spares=2   devices=/dev/sdd2,/dev/sdb2,/dev/sdf2,/dev/sde2,/dev/sdc2,/dev/sda2
ARRAY /dev/md/2 level=raid6 metadata=1.2 num-devices=6 UUID=74c85cf7:e8d75a3f:6daa34d5:db1226e7 name=shadow.arkusa.com:2
   devices=/dev/sdd3,/dev/sdb3,/dev/sdf3,/dev/sde3,/dev/sdc3,/dev/sda3
shadow# blkid /dev/md1
/dev/md1: UUID="rLojRP-Ms7b-VKlA-p7Cj-n1nL-0MAy-nLy4Ty"

●そして最後にINITRAMFS内の/etc/mdadm.confのRAIDレベル、ディスク台数、UUID
shadow# mkdir /tmp/initrd
shadow# cd /tmp/initrd
shadow# cp /boot/initramfs-2.6.34.9-69.fc13.i686.PAE.img .
shadow# gunzip -c initramfs-2.6.34.9-69.fc13.i686.PAE.img | cpio -i
shadow# cat etc/mdadm.conf
ARRAY /dev/md1 level=raid5 num-devices=4 UUID=cc8c918f:5b720e12:5c2a14a8:2e918d19


ファイルシステム、あるいはそのファイルシステムが存在するLVMやRAIDを認識する識別子には以下の3種類がある。
  1. デバイスファイル名(例: /dev/md1)
  2. ラベル(例: LABEL=/)
  3. UUID
下に行くほどモダンで曖昧さが薄れる。

驚くべきことに、UUIDには少なくとも3種類の表現方法があるらしい。
  1. UUID="67947017-6b60-1dbe-8457-1880d35f9d0e" 「blkid」の出力など
  2. UUID="67947017:6b601dbe:84571880:d35f9d0e" 「mdadm -Esv」の出力など
  3. UUID="nGR8l1-6jaI-SNbg-ILSp-Oh8o-53bM-oXm45a" LVMの「blkid」の出力、/etc/lvm/archive/xxxx.vgなど
上記の三つは、同じオブジェクトのUUIDで、全て同じ値を示す。1と2はハイフンとコロンの桁区切りの違いだけのようだが、3はBase64風で何だかよく分からない。

なお、Universally Unique IDと言っても、RAIDの構成メンバの個々のUUIDとRAIDそのもののUUIDは全て同じ。例えば、/dev/md0が/dev/sda1、/dev/sdb1、/dev/sdc1、/dev/sdd1から成り立っていると、これらの5台のオブジェクトのUUIDは全て共通。

システムをFedoraのCDのレスキューモードで起動。ネットワークを使えるようにしておくと便利。レスキューするファイルシステムはマウントしないようにする。

まずRAIDを再構築。RAID5以上の作成には--createオプションを使う。以前にRAIDに使われていたディスクを使うときはシグネチャを検査して上書きの警告をしてくるが、「OK」を選択する。
shadow# mdadm --create /dev/md1 --level=5 --raid-devices=4 /dev/sda2 /dev/sdb2 /dev/sdc2 /dev/sdd2

このままではランダムなUUIDが与えられてしまう。
shadow# mdadm -Esv
ARRAY /dev/md1 level=raid5 num-devices=4 UUID=86f7ac38:284c3bb5:bfe78010:bc810f04

他の正常なRAIDデバイス名が「/dev/md/N」の形で表示されるのに「/dev/md1」であり、「metadata」の表示もない。
そこでちょっとトリックを使う。
一旦RAIDを停止し--metadataオプションを追加して作成し直し。
shadow# mdadm --stop /dev/md1
shadow# mdadm --create /dev/md1 --metadata=1.1 --level=5 --raid-devices=4 /dev/sda2 /dev/sdb2 /dev/sdc2 /dev/sdd2

もう一度RAIDを停止してUUIDをアップデートするオプションでアセンブルし直し。
shadow# mdadm --stop /dev/md1
shadow# mdadm --assemble /dev/md1 --update=uuid --uuid=cc8c918f:5b720e12:5c2a14a8:2e918d19 /dev/sda2 /dev/sdb2 /dev/sdc2 /dev/sdd2
shadow# mdadm -Esv

ARRAY /dev/md/1 level=raid5 metadata=1.1 num-devices=4 UUID=cc8c918f:5b720e12:5c2a14a8:2e918d19
    devices=/dev/sda2,/dev/sdd2,/dev/sdc2,/dev/sdb2 
shadow# blkid /dev/md1
/dev/md1: UUID="rLojRP-Ms7b-VKlA-p7Cj-n1nL-0MAy-nLy4Ty"

めでたくRAIDが正しいUUIDで復活した。

mdadmの--addオプションを使って、残りの2台のディスクをスペアとして参加させておく。スペアがあれば、アクティブなディスクのどれかが死んだときにすぐに代替ディスクとして組み込まれ、リシンクが実行される。リシンク完了までに他のアクティブディスクが死ななければデータは完璧に保存される。実は、RAID6はこのリシンクの作業をリアルタイムに分散して実行していると見ることもできる。
shadow# mdadm /dev/md1--add /dev/sde2 /dev/sdf2


次に、LVMの設定ファイルを使ってLVMを復活させる。1番目のvgrestoreでレイアウトを復活させ,2番目のvgchangeでLVMを起動する。
shadow# lvm vgcfgrestore --file vg_shadow_00000.vg vg_shadow
shadow# lvm vgchange -a y vg_shadow
  Restored volume group vg_shadow
  2 logical volume(s) in volume group "vg_shadow" now active
blkid /dev/md1
/dev/md1: UUID="rLojRP-Ms7b-VKlA-p7Cj-n1nL-0MAy-nLy4Ty" TYPE="LVM2_member"

このLVM設定ファイルにはルート(/)と/varの二つのLVが記述されていたので、それぞれが作成される。UUIDはランダムな値が設定されるが、これはどことも整合させる必要がないのでそのまま。
shadow# ls -l /dev/mapper
total 0
crw-rw----. 1 root root  10, 62 2014-02-04 07:41 control
brw-rw----. 1 root disk 253,  1 2014-02-04 08:27 vg_shadow-LogVol00
brw-rw----. 1 root disk 253,  0 2014-02-04 08:27 vg_shadow-LogVol01_var

ここまできたらほとんどできたも同然。それぞれのLVにmkfsでファイルシステムを作り、バックアップからファイルをリストアして、レスキューCDの/mnt/sysimageに仮マウントできることを確認したらリブート。

まあ、人間必ずチョンボがあるので、一発で完全回復はできないかもしれないが、完璧なバックアップさえあれば順序よく誤りを潰していけば必ず復活できる。

ちなみに小生が犯した最後のエラーは、バックアップに/var/runを含めていなかったこと。この頃のFedora13のディストロでは、/var/run内にサブディレクトリを作るデーモンのいくつかは、サブディレクトリをランタイムに作成せずインスタレーション時に作成することを仮定していたので、それらのデーモンが機動できなかった。これは、他のFedora13の/var/runをそっくりコピーして完治。バックアップファイルセットから/var/runの除外を消すことも実施して完璧な修復ができた。

教訓をもう一度。

ルートファイルシステムはブートローダと密接な関係があるので決して構成を変えてはいけない。

延べ3日の作業は高い代償だったが、RAIDとLVMの修復の経験を積んだので価値ありとする。

2014年1月7日

Raspberry Piにリアルタイムクロックをインストールする

Raspberry Pi(以下RPi)は、コストを抑えるためか、電池でバックアップされたリアルタイムクロックを実装していない。このため、インターネットに接続されていない、正確にはNTPサーバにアクセスできない環境では、時計が例の1970年の元旦にリセットしてしまい、具合が悪い。前回のドライブレコーダの記事に使おうとしているRPiは「時々」インターネットに繋がる可能性があるだけなので、何とかしなければならない。
 
 
Amazonで検索すると、うってつけの製品が見つかった。SunFounder DS3231という小さなPCボードで、I2CでRPi(やArduino)のI/Oポートに直接装着できる。単価$6という価格も嬉しい。さっそく買ってみた。
 
RPiでの使用方法はこちらのサイトに解説されており(右の製品写真もこのサイトから拝借した)、それに従えばちゃんと動くが、少々読みにくいので以下に読み下し解説を再掲載してみる。

ハードウェアの実装はとても簡単。写真のようにI/Oポートに向きを正しく差し込んでお終い。

ソフトウェアは、以下の二つのファイルを変更し、
  • /etc/init.d/hwclock.sh
  • /etc/modprobe.d/raspi-blacklist.con
以下のファイルを追加する。
  • /etc/cron.hourly/sync-hwclock
いちいちエディタで修正するのは面倒なので、パッチファイルを作ったので使ってください。
 
/tmp/ds3231にパッチファイルをダウンロードして、patchを実行。





root@pi:~# cd /
root@pi:/# patch --dry-run -p0 < /tmp/ds3231.patch
 問題のないことを確認してね
root@pi:/# patch -p0 < /tmp/ds3231.patch 
実は、最後の追加ファイル(/etc/cron.hourly/sync-hwclock)は原典には含まれていない。定期的にntptraceを実行して、NTPが生きているならリアルタイムクロックをNTPに同期させるごく簡単なスクリプト。
 

で、OSの設定を変更

ファイルにパッチを当てただけではまだ動かない。以下の手順を実行してOSの設定を変更しなければならない。 

まず、パッチを当てたhwclock.shをイネーブルしてリブート。
root@pi:~# update-rc.d hwclock.sh enable
root@pi:~# reboot

この段階で、/dev/rtc0という新しいデバイスファイルが出来上がっていることを確認。
確認できたら、今一度タイムゾーンの設定を確認。
root@pi:~# dpkg-reconfigure tzdata
 
もうリアルタイムクロックが使えるので、NTPで同期してある現在の時刻をリアルタイムクロックに書き込んで、もともとのダミーのリアルタイムクロックをディセーブルすると出来上がり。
root@pi:~# heclock -w
root@pi:~# update-rc.d fake-hwclock disable
root@pi:~# reboot

原典では以下も実行してNTPを完全に殺しているが、時々でもインターネットに繋がる可能性があるのなら、NTPで時刻を常に同期させる方がよいのでここでは実行しない。もしNTPを完全に殺すなら、上記で作成したcronファイルは削除してください。
root@pi:~# update-rc.d ntp disable
  
リアルタイムクロックの動作確認は、本当はネットワークを外してリブートしてOSの時計がちゃんと設定されていることを確認すべきだが、ハードウェアクロックの動作確認だけなら以下で十分。
root@pi:~# hwclock --show


簡単でしたね。