tool
Android/iOS向けテストアプリ配信ツール 「EMLauncher」を公開しました
先日、TestFlightのAndroidサポート終了、Appleによる買収といったニュースが世間を騒がせましたが、皆さんテストアプリの配信はどうしていますか?
KLabでは自社製のテストアプリ配信ツール「EMLauncher」を使用しているのですが、せっかくなのでOSSとして公開することにしました。
今すぐ試す
今すぐ試したい方のために、セットアップ済みのAWS EC2イメージを用意しました。
インスタンスを起動後、設定ファイルのAWSアカウント情報を編集し、S3のバケットを作成してください。
(設定の詳細はconfigディレクトリのサンプルをご覧ください)
/home/ohoflight/emlauncher/config/emlauncher_config.php ---- (略) /** AWSの設定 */ 'aws' => array( /** * APIアクセスのためのKeyとSecret. */ 'key' => 'xxxxxxxx', 'secret' => 'xxxxxxxx', /** S3のRegion. */ 'region' => Aws\Common\Enum\Region::TOKYO, /** S3のbucket名. 予め作成しておく. */ 'bucket_name' => 'emlauncher', ), ), (略)
設定を終えたらそのインスタンスにwebブラウザでアクセスしてください。ログインページが表示されます。
初期状態ではメールアドレス"test@example.com", パスワード"test"でログインできます。
特徴
どこからでもアクセス可能なwebアプリケーション
専用アプリをインストールすることなく、ブラウザでアクセスするだけでどんな端末からもすぐに使えます。
iOSアプリのOTA(Over The Air)配信にも対応していますし、PCからアクセスすれば apk/ipa ファイルをダウンロードできます。
ただ、TestFlightやDeployGateと違い、端末の詳細な情報やクラッシュレポートは取得できません。
EC2+S3のシンプルな構成
EC2のインスタンス1台とS3のバケット1つで動作します。
KLabではマイクロインスタンス1台で運用していますが、比較的安定して動いています。
Google OAuthによるログイン
KLabではGoogle Appsを利用しており、全社員が独自ドメインのgoogleアカウントをもっています。
このアカウントをログインに利用することで、面倒なユーザ管理をなくしました。
なおKLabでは無効にしていますが、メールアドレスとパスワードによる認証も実装してあります。
TestFlightライクなアップロードAPI
apk/ipaファイルのアップロードはブラウザからだけでなく、TestFlightライクなAPIも用意してあります。
さらにJenkinsプラグインも用意したので、ビルドプロセスに簡単に組み込めます。
それではよりよいアプリ開発ライフを!
(3/11追記) iOS 7.1へのOTAインストールについて
iOS 7.1へのipaのOTAインストールにはSSL通信が必須となりました。
サンプルAMIを利用する場合は、信頼できるSSLサーバ証明書を設置した上で次の設定をしてください。
- セキュリティグループでHTTPSの接続を許可
emlauncher_config.php
でenable_https
の設定を変更$emlauncher_config['ec2']['enable_https'] = true;
なお、自己署名証明書は利用できません。ご注意ください。
iPXE を導入した記念に blog を書いてみたけど、タイトルが悩ましいので、この記事のタイトル募集中!
(自分の書いたタイトルはダメ出しされたので、募集することにしてみました(^^;;記事を読んでいただいて、もし、よいタイトルを思いついたら、ぜひ、コメントにでもお願いしますm(_ _)m。次回のタイトルに使わせていただくかも(^^)
- PXEとは、ネットワークブートのための仕組みの1つ。NIC上のフラッシュROM に書き込まれている。ネットワークブートに必要なファイルの取得には、DHCPとTFTP を用いる
- iPXE(gPXE)とは、PXEを拡張したオープンソースなネットワークブートファームウェア/ローダ
iPXE(gPXE)には、下記のような特徴があります。
- MACアドレスの他にもサーバのDMI情報を取得できる
- コマンドが用意されていてそのコマンドで組んだスクリプトを利用可能
- httpでのリクエストをサポートしている
つまり・・・
- DMI情報を取得できるということは、そのサーバがどんな機種なのか分かる
- スクリプトから取得した機種情報を使ったリクエストをすることができそう
- http+cgiで、リクエストに応じて動的にレスポンスを返すことができそう
どうでしょう。試してみたくなりませんか?
ではでは、これから、実際にiPXEを触っていくとしましょう。
iPXE(gPXE)の紹介
gPXEのほうがよく知られているかもしれませんが、gPXEの開発が止まり、その後釜として始まったプロジェクトがiPXEとなります。
- iPXE: http://ipxe.org/
- gPXE: http://etherboot.org/wiki/start
本来は、PXEと同じくNICのROMに焼いて使用するようですが、そうすると、元からあるPXEは当然消えてしまいますし、数十台、数百台のサーバを運用している場合に、すべてのNICに適用するとしたら、気が遠くなりそうです。しかしながら、よくみてみると、PXEからiPXEをチェインロードできるとあります。この方法を使えば、NICのROMに手を加えることなく、iPXEを利用することができそうです。
iPXEを実際に起動してみて、触ってみよう
PXEからチェインロードする方法でiPXEを試しに起動させてみます。
ここでは、isc-dhcpd,tftpd,thttpd(cgiは、bashスクリプト)を使用し、設定は、以下のようになっているとします。
(ここでは、iPXE以外の細かい設定に関しては、割愛します)
・tftpd,thttpdサーバのIPアドレス
192.168.0.1
・tftpd,thttpdルートディレクトリ
/tftp/
・thttpdのcgiディレクトリ
/tftp/cgi-bin/
・iPXE関連ファイルの格納ディレクトリ
/tftp/iPXE/
まずは、PXEチェインロード用のバイナリであるundionly.kpxeを以下のページ内のリンクからDLし、/tftp/iPXE/undionly.kpxeに保存します。
http://ipxe.org/howto/chainloading
上記ページのリンク先に紹介されているように、DHCPの設定ファイルに以下のように記述を追加します。
if exists user-class and option user-class = "iPXE" { filename "http://192.168.0.1/iPXE/boot.ipxe"; } else { filename "iPXE/undionly.kpxe"; } next-server 192.168.0.1;
filenameの値を、iPXEからのリクエストにはiPXEの起動用スクリプト(boot.ipxe)を返し、iPXE以外のリクエストにはiPXEをロードするためのundionly.kpxeを返すようにしています。
これにより、以下のような流れとなります。
PXE起動
↓
iPXE(undionly.kpxe)を取得してロード
↓
iPXE起動
↓
起動用スクリプト(boot.ipxe)を取得してロード
↓
boot.ipxeの内容を実行
/tftp/iPXE/boot.ipxeは、以下のように記述しておきます。
#!ipxe imgfree chain http://${next-server}/cgi-bin/boot_ipxe.cgi?type=${product:uristring}&mac=${mac}
boot.ipxeの中身の説明はとりあえず一旦おいといて、実際にマシンをPXEブートしてみると、以下のようになります。
Intel(R) Boot Agent GE v1.2.40 Copyright (C) 1997-2006, Intel Corporation CLIENT MAC ADDR: XX XX XX XX XX XX GUID: xxxxxxxxxxxxxxxxxxxxxxxxxxx CLIENT IP: 192.168.0.xx MASK: 255.255.255.0 DHCP IP: 192.168.0.1 GATEWAY IP: 192.168.0.xx PXE->EB: !PXE at 97CA:0070, entry point at 97CA:0106 UNDI code segment 97CA:3F20, data segment 919C:62E0 (582-623kB) UNDI device is PCI 07:00.0, type DIX+802.3 582kB free base memory after PXE unload iPXE initialising devices...ok iPXE 1.0.0+ -- Open Source Network Boot Firmware -- http://ipxe.org Features: HTTP iSCSI DNS TFTP AoE bzImage ELF MBOOT PXE PXEXT Menu Press Ctrl-B for the iPXE command line...
iPXEがチェインロード起動されると、自動的にboot.ipxeを取得して実行となるのですが、インタラクティブモードに入ると、自動実行は行わず、手動でコマンド入力をすることが出きるようになります。"Press Ctrl-B for the iPXE command line..."の表示がされたら、すかさず、Ctrl+Bを入力してみましょう。すると、下記のプロンプトが表示され、コマンドの入力ができるようになります。では、試しに、いくつかコマンドを入力してみましょう。
iPXE> # マックアドレスを表示 iPXE> show mac net0/mac:hex = xx:xx:xx:xx:xx:xx # DHCPにてIPアドレスを取得 iPXE> dhcp DHCP (net0 xx:xx:xx:xx:xx:xx)... ok # IPアドレスを表示 iPXE> show ip net0.dhcp/ip:ipv4 = 192.168.0.xx # DHCPで設定されたfilenameを表示 iPXE> show filename net0.dhcp/filename:string = http://192.168.0.1/iPXE/boot.ipxe # DHCPで設定されたnext-serverを表示 iPXE> show next-server net0.dhcp/next-server:ipv4 = 192.168.0.1 # サーバの機種情報を表示 iPXE> show product smbios/product:string = Express5800/i120Ra-e1 [N8100-1482Y] # urlエンコードして表示 iPXE> show product:uristring smbios/product:uristring = Express5800%2Fi120Ra-e1%20%5BN8100-1482Y%5D
サーバの機種情報も無事取得できているようです。次は、boot.ipxeをhttp経由で取得できるかどうか試してみます。
iPXE> imgfetch ${filename} http://192.168.0.1/iPXE/boot.ipxe.... ok iPXE> imgstat boot.ipxe : 102 bytes
無事、取得できているようですので、boot.ipxeの内容を手動で試してみましょう。ここでは、まだ、cgiを準備していないので、エラーとなりますが、どのようにアクセスされるかの確認ができます。
iPXE> imgfetch http://${next-server}/cgi-bin/boot_ipxe.cgi?type=${product:uristring}&mac=${mac} http://192.168.0.1/cgi-bin/boo_ipxe.cgi?type=Express5800%2Fi120Ra-e1%20%5BN8100-1482Y%5D&mac=xx%3Axx%3Axx%3Axx%3Axx%3Axx... No such file or directory (http://ipxe.org/2d0c613b)
機種情報とMACアドレスが代入されてhttpリクエストをしていることが確認できました。
もうお分かりのようにboot.ipxeは、機種情報とMACアドレスを取得し、それをboot_ipxe.cgiにクエリ文字列として付与して叩き、そのレスポンスの内容を実行するスクリプトということになります。(${product}でも、自動的にurlエンコードされるようですが、念のため、uristringでの出力指定をしています。)
では、サーバ起動用の簡単なiPXEスクリプトを、機種情報やMACアドレスを元にboot_ipxe.cgiで動的に生成して、実際にサーバを起動させてみましょう。
Linuxを起動してみる
MACアドレスと機種情報にしたがって、initrdとkernelを決定して起動するだけの簡単なboot_ipxe.cgiを用意してみます。
ちなみに、iPXEでは確認していませんが、gPXEでは、Content-Lengthを指定しないとうまく動作しなかったため、指定するようにしています。
#!/bin/bash # クエリ文字列は、$QUERY_STRINGに格納されている # 変数type,macをクエリ文字列から生成する for q in $(echo ${QUERY_STRING} | tr "&" " "); do eval $q done case $type in *Express*) initrd_file=initrd.gz ;; esac case $mac in *xx%3Axx%3Axx%3Axx%3Axx%3Axx*) kernel_file=vmlinuz ;; esac body=$(#!ipxe initrd http://\${next-server}/${initrd_file} kernel http://\${next-server}/${kernel_file} boot ) echo "Content-type: text/plain" echo "Connection: close" echo "Content-Length: $(echo -e "${body}" | wc -c)" echo "" echo -e "${body}"
wgetなどで、先ほどのiPXEが出力したパスを叩いて確認してみると、以下のような出力となります。
$ wget -q -O - "http://192.168.0.1/cgi-bin/boo_ipxe.cgi?type=Express5800%2Fi120Ra-e1%20%5BN8100-1482Y%5D&mac=xx%3Axx%3Axx%3Axx%3Axx%3Axx" #!ipxe initrd http://${next-server}/initrd.gz kernel http://${next-server}/vmlinuz boot
では、/tftp/以下に適当なvmlinuzとinitrd.gzを準備して、マシンを再起動させてみます。
Intel(R) Boot Agent GE v1.2.40 Copyright (C) 1997-2006, Intel Corporation CLIENT MAC ADDR: XX XX XX XX XX XX GUID: xxxxxxxxxxxxxxxxxxxxxxxxxxx CLIENT IP: 192.168.0.xx MASK: 255.255.255.0 DHCP IP: 192.168.0.1 GATEWAY IP: 192.168.0.xx PXE->EB: !PXE at 97CA:0070, entry point at 97CA:0106 UNDI code segment 97CA:3F20, data segment 919C:62E0 (582-623kB) UNDI device is PCI 07:00.0, type DIX+802.3 582kB free base memory after PXE unload iPXE initialising devices...ok iPXE 1.0.0+ -- Open Source Network Boot Firmware -- http://ipxe.org Features: HTTP iSCSI DNS TFTP AoE bzImage ELF MBOOT PXE PXEXT Menu net0: xx:xx:xx:xx:xx:xx using undionly on UNDI-PCI07:00.0 (open) DHCP (net0 xx:xx:xx:xx:xx:xx)... ok net0: 192.168.0.xx/255.255.255.0 gw 192.168.0.xx Next server: 192.168.0.1 Filename: http://192.168.0.1/iPXE/boot.ipxe http://192.168.0.1/iPXE/boot.ipxe... ok http://192.168.0.1/cgi-bin/boot_ipxe.cgi?type=type=Express5800%2Fi120Ra-e1%20%5BN8100-1482Y%5D&mac=xx%3Axx%3Axx%3Axx%3Axx%3Axx...ok http://192.168.0.1/initrd.gz... ok http://192.168.0.1/vmlinuz... ok [ 0.000000] Initializing cgroup subsys cpuset [ 0.000000] Initializing cgroup subsys cpu [ 0.000000] Linux version 3.0.21 ..... .... .... ....
カーネルがロードされ、無事起動できたようです。
実際の運用で使ってみるとすると・・・
DSASでは、ほとんどのサーバがPXEブート+PXELINUXを利用してディスクレスサーバとして起動されるようになっています。
また、リモートからのサーバ管理にIPMIを利用しており、SOL(Serial over Lan)対応シリアルポートを、PXELINUXの起動設定ファイルにて記述することにより、IPMIのSOL経由にてPXELINUXの起動メニューの表示/編集やLinuxの起動メッセージの表示ができるようにしています。
iPXEは、独自メニューを作成可能なのですが、残念ながら、PXELINUXやGRUBのように、起動時にインタラクティブに編集することは出来ないようです。
ですが、PXELINUXのpxelinux.0やSYSLINUXのcom32をロードして実行することができます(com32に関しては、SYSLINUXのver4.xxのcom32ファイルの直接実行はできず、ver3.86以下のものを使用する必要があります)。
そこで、pxelinux.0やmenu.c32(com32ファイルで用意されているメニュー用ファイル)を利用し、そのメニュー設定をiPXEで取得できる情報から動的に生成すれば、サーバ機種によって適切な起動カーネルパラメータを指定することができそうです。また、起動時にインタラクティブにパラメータを編集するといったこともいけそうです。
さて、今回は、ここまでとして、次回ではmenu.c32のメニュー機能をiPXEから利用したり、IPMI SOL経由で表示したりする例を紹介できたらと思っています。
「crashdmesg」の仕組み 〜vmcoreからリングバッファ領域を取得する方法〜
前回公開した「crashdmesg」の仕組みを順を追ってご紹介します。
vmcoreを覗いてみる
kexec+kdumpを使って取得したvmcoreの解析には、gdbを拡張したcrashコマンドを利用します。
crashコマンドは、デバッグシンボル付きのカーネルを準備して、次のように実行します。
$ crash -s vmlinux.debug vmcore
ダンプの調査をする場合には、このままcrashコマンドを使って作業を進めるのですが、今回はvmcoreファイルそのものに注目してみます。
このvmcoreファイル、正体はELF形式のコアファイルです。
readelfコマンドを使うとELFフォーマットのヘッダに書かれた情報を表示できます。vmcoreをreadelfで開いてみると、次のような情報を見ることができます。
$ readelf -a vmcore ELF Header: Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 Class: ELF64 Data: 2's complement, little endian Version: 1 (current) OS/ABI: UNIX - System V ABI Version: 0 Type: CORE (Core file) Machine: Advanced Micro Devices X86-64 Version: 0x1 Entry point address: 0x0 Start of program headers: 64 (bytes into file) Start of section headers: 0 (bytes into file) Flags: 0x0 Size of this header: 64 (bytes) Size of program headers: 56 (bytes) Number of program headers: 5 Size of section headers: 0 (bytes) Number of section headers: 0 Section header string table index: 0 There are no sections in this file. There are no sections in this file. Program Headers: Type Offset VirtAddr PhysAddr FileSiz MemSiz Flags Align NOTE 0x0000000000000158 0x0000000000000000 0x0000000000000000 0x000000000000083c 0x000000000000083c 0 LOAD 0x0000000000000994 0xffffffff81000000 0x0000000001000000 0x0000000002125000 0x0000000002125000 RWE 0 LOAD 0x0000000002125994 0xffff880000000000 0x0000000000000000 0x00000000000a0000 0x00000000000a0000 RWE 0 LOAD 0x00000000021c5994 0xffff880000100000 0x0000000000100000 0x0000000018f00000 0x0000000018f00000 RWE 0 LOAD 0x000000001b0c5994 0xffff88001f000000 0x000000001f000000 0x0000000000ffd000 0x0000000000ffd000 RWE 0 There is no dynamic section in this file. There are no relocations in this file. There are no unwind sections in this file. No version information found in this file. Notes at offset 0x00000158 with length 0x0000083c: Owner Data size Description CORE 0x00000150 NT_PRSTATUS (prstatus structure) CORE 0x00000150 NT_PRSTATUS (prstatus structure) VMCOREINFO 0x0000055a Unknown note type: (0x00000000)
LOADセグメント内に、メモリ内容がダンプされています。
/proc/iomemやカーネルドキュメント(Documentation/x86/x86_64/mm.txt)と見比べてみると、カーネル本体がロードされている領域、メモリの先頭から640KBまでの領域、残りのメモリのうちセカンドカーネルが使用する領域を除いた部分に分割して格納されているようです。
ELFプログラムヘッダを解析すれば、各LOADセグメントがどの物理・仮想アドレスに対応するかと、vmcoreファイル中でのオフセット位置 がわかります。シンボル情報等から目的の値が格納されている仮想アドレスがわかれば、vmcoreから内容を取得することができそうです。
カーネルログ領域(リングバッファ)を探す
今回の目的である、カーネルのリングバッファ領域がどこにあるのか探してみましょう
リングバッファの定義や、アクセスする関数は kernel/printk.c に書かれています。
(以降のカーネルソースの引用で、行頭に書かれている行番号は、バージョン 2.6.37 時点での番号です。)
kernel/printk.c: 58 #define __LOG_BUF_LEN (1 << CONFIG_LOG_BUF_SHIFT) 146 static char __log_buf[__LOG_BUF_LEN]; 147 static char *log_buf = __log_buf; 148 static int log_buf_len = __LOG_BUF_LEN;
バッファのサイズはカーネルビルド時にCONFIG_LOG_BUF_SHIFTで指定します。
カーネル上でもlog_buf_len変数にバイト単位で格納されていて、バッファ本体はそのサイズ分の配列として__log_bufと定義されています。
バッファサイズlog_buf_lenとバッファ本体__log_buf(もしくはlog_buf)が分かればバッファ領域を取得することができそうです。
リングバッファが循環する仕組みを探る
運用中のサーバのカーネルメッセージをdmesgコマンドで表示したことがある人は、カーネルメッセージの量が多くなってくると古いログが消えていくのを見たことがあると思います。
カーネルメッセージは、リングバッファという仕組みで、一定のサイズ内で循環するように書きこむことで古いログを捨てながら保存されていきます。
この仕組を探ってみましょう。
kernel/printk.c: 109 #define LOG_BUF_MASK (log_buf_len-1) 110 #define LOG_BUF(idx) (log_buf[(idx) & LOG_BUF_MASK]) 116 static unsigned log_start; /* Index into log_buf: next char to be read by syslog() */ 117 static unsigned con_start; /* Index into log_buf: next char to be sent to consoles */ 118 static unsigned log_end; /* Index into log_buf: most-recently-written-char + 1 */ 149 static unsigned logged_chars; /* Number of chars produced since last read+clear operation */ 548 static void emit_log_char(char c) 549 { 550 LOG_BUF(log_end) = c; 551 log_end++; 552 if (log_end - log_start > log_buf_len) 553 log_start = log_end - log_buf_len; 554 if (log_end - con_start > log_buf_len) 555 con_start = log_end - log_buf_len; 556 if (logged_chars < log_buf_len) 557 logged_chars++; 558 }
バッファへの書き込みを循環させるロジックのキモは、LOG_BUF(idx)というマクロです。
LOG_BUF(idx) マクロを使用してログメッセージを書きこむ際、単純に log_buf[idx] に書くのではなく、インデックスに log_buf_len - 1とのAND演算を行っています。
バッファサイズは、2のべき乗バイトに制限されているので、LOG_BUF_MASK はちょうど log_buf のインデックスの範囲(0 〜 log_buf_len-1)を選択するマスクになるため、バッファサイズを超えた部分の桁を無視することができます。
こうして、idx がバッファサイズを越えても、オーバーフローした桁を無視するので先頭に戻ってくるのです。
printk等からのバッファへの書き込みはemit_log_char()関数が使われますが、その中で特にバッファ領域のダンプに必要な情報として、log_endとlogged_charsが更新されます。
バッファに書きこむたびに、log_endは単純増加しますが、logged_charsはlog_buf_lenまで到達すると、それ以後の書き込みでは増えません。
実際に、バッファ領域をダンプするには、logged_charsの値をもとに、バッファがまだ一杯ではない状態と、バッファが一杯で循環して書き込まれている状態の2パターンを考える必要があります。
-
バッファが一杯でない場合:
バッファ領域の先頭から、logged_charsまでの範囲 -
バッファが一杯で循環書き込みしている場合:
前半(バッファ領域的には後半)部分、log_end & (log_buf_len - 1) から log_buf_len - 1 までの範囲と、
後半部分、バッファの先頭から log_end & (log_buf_len - 1) - 1までの範囲
シンボル情報を探す
log_buf、log_buf_len、logged_chars、log_endの4つの値が分かればリングバッファを正しい順序でダンプできそうなので、この4つの値が格納されている仮想アドレスを探してみましょう。
カーネルビルド時に生成されるSystem.mapからシンボル情報をさがす方法と、同様の情報をカーネル本体(vmlinux)から探す方法の2つを試してみます。
■ System.mapから探す方法
$ egrep " (log_buf|log_buf_len|logged_chars|log_end)$" /boot/System.map ffffffff82c16b60 d log_buf_len ffffffff82c16b68 d log_buf ffffffff82d2a080 d log_buf ffffffff82edb300 b log_end ffffffff82edb3e0 b logged_chars
■ vmlinuxをobjdumpする方法
$ objdump -t /boot/vmlinux |egrep " (log_buf|log_buf_len|logged_chars|log_end)$" ffffffff82c16b60 l O .data 0000000000000004 log_buf_len ffffffff82c16b68 l O .data 0000000000000008 log_buf ffffffff82edb300 l O .bss 0000000000000004 log_end ffffffff82edb3e0 l O .bss 0000000000000004 logged_chars ffffffff82d2a080 l O .data 0000000000000020 log_buf
(サイズの違うlog_bufが2個見つかっていますが、0xffffffff82d2a080にあるlog_bufはリングバッファとは無関係なネットワーク関連の構造体のようです。)
これで、各変数の格納されている仮想アドレスが判明したので、vmcore内でこの変数が格納されているLOADセグメントを探して読み出せば、リングバッファのダンプが可能になります。
VMCOREINFOからシンボル情報を取得する
リングバッファをダンプするのに必要な情報は集まりましたが、ダンプするにはSystem.mapかvmlinuxが必要です。
crashdmesgは、クラッシュカーネル上で実行することを前提に考えているので、クラッシュしたカーネルのSystem.mapやvmlinuxが読めるとは限りません。
ここで、先程、readelfで読み取ったvmcoreの情報を見直してみます。
NOTEセグメントの中に、VMCOREINFOという意味深なデータが入っています。
VMCOREINFO 0x0000055a Unknown note type: (0x00000000)
ダンプしてみると、このような内容で、CRASHTIMEを除き、"NAME=num\n"もしくは"TYPE(name)=num\n"という形式のテキストで、いくつかの情報が出力されています。
log_buf等の情報もばっちり記録されています。
VMCOREINFOの内容は、カーネルソースの各所からVMCOREINFO_SYMBOL(name)等のマクロを呼ぶことで記録しています。 ちなみに、CRASHTIMEの値だけは、panic関数から呼ばれるcrash_kexec関数の処理内で追記されるため、カーネルパニックが起こった時間の正確な記録として使えそうです。
crashdmesgでは、vmcoreのNOTEセグメントに保存されているVMCOREINFOのシンボル情報を利用することで、vmcoreファイル以外の追加情報を読み込むこと無く、リングバッファをダンプできるようになっています。
次回は、セカンドカーネル上でこのcrashdmesgを利用するためのinitramfsの作り方等をご紹介しようと思います。
クラッシュダンプからカーネルメッセージを取り出すツール「crashdmesg」を作りました
Linuxカーネルには、カーネルパニック時にkexecを使ってダンプ取得用のカーネル(セカンドカーネル)を起動する仕組みがあります。
このセカンドカーネルは予めリザーブされたメモリ内で起動するため、クラッシュしたカーネルが処理していたメモリの内容はそのまま残っていて、procファイルシステム経由でクラッシュダンプを取得する事ができます。
このDSASブログでも、以前「Linuxでクラッシュダンプを採取(1) 〜 kexec + kdump を使ってみる 〜」と言うタイトルでクラッシュダンプの取得方法をご紹介しました。
「crashdmesg」は、kexec+kdumpで保存したクラッシュダンプから、カーネルメッセージの内容を取り出すツールです。
デバッガと比べてはるかに軽量なため、セカンドカーネル上で直接/proc/vmcoreからカーネルメッセージを取り出すこともできます。
最近のクラッシュダンプ事情
クラッシュダンプにはサーバ上のメモリの内容がほとんど保存されているため、カーネルメッセージのほか、クラッシュ時に稼働していたプロセスやネットワークの状態など、多くの情報を取得することができます。
カーネルパニックでサーバがダウンした場合、syslog等でのカーネルメッセージの転送は間に合わず、ログが全くない状態で原因究明を行わなければならないことが多く、クラッシュダンプを取得する仕組みは、カーネルに起因するトラブル解析の強い味方です。
しかし、クラッシュダンプを取得する方法にはデメリットもあります。
サーバの搭載メモリのほぼ全てをダンプするため、クラッシュダンプのサイズも搭載メモリに合わせて大きくなり、保存に時間がかかるのです。
例えば弊社では、メモリを72GB搭載して運用しているサーバがあり、この場合では72GB近いダンプファイルをローカルディスクもしくはネットワーク上の他のサーバに転送する必要があります。
転送が終わるまでは、サーバの再起動や調査を行うことができず、復旧が遅れてしまいます。
最小限のダウンタイムで情報集め
そもそも、巨大なダンプファイルを転送する時間がないのであれば、セカンドカーネル上でクラッシュダンプを解析してしまい、結果だけを転送するという方法を思いつきました。
クラッシュダンプを解析するcrashコマンドに、予めカーネルメッセージのダンプや、プロセス、ネットワーク状態等の状況を出力するスクリプトを実行させて、結果のテキストデータだけを転送するという作戦です。
パニックのメッセージやスタックトレースの出力に応じたデバッグはできなくなりますが、カーネルのバグフィックス情報を調べる手がかりぐらいは読み取れるのではないかと考えたのです。
しかし、crashコマンドを実行するには、100MB近いサイズになるデバッグシンボル付きのカーネルが必要になるほか、crashコマンドの実行自体もかなりのメモリを必要とします。
セカンドカーネルは予めリザーブした領域上で動くため、非常時にcrashコマンドを実行するためだけに多量のメモリをリザーブするわけにもいかず断念しました。
crashdmesg
そこで思い切って、取得する情報をカーネルメッセージのバッファ領域だけに絞り、カーネルダンプファイルから直接情報を取得することができないか調べてみました。
すると、いくつかのシンボル情報が取得出来れば、クラッシュダンプからカーネルメッセージのバッファ領域を拾うことができそうだと分かり、「crashdmesg」というツールを作ってみました。
その名のとおり、dmesgコマンドのクラッシュダンプ版をイメージしていて、/proc/vmcoreもしくは引数に指定したダンプファイルからカーネルメッセージをダンプして標準出力に出力します。
軽量なプログラムですので、セカンドカーネル上の僅かなメモリの中でも使うことができます。
しかも、ダンプに必要なシンボル情報をvmcore内から読み取るため、デバッグシンボル付きのカーネルやSystem.mapを用意する必要がありません。
最後に
crashdmesgのソースコードは、githubで公開しています。
https://github.com/hiro-dSn/crashdmesgcrashdmesgの仕組み解説や、kexec+kdump使用のコツなども、追ってこのDSASブログに掲載したいと思います。
まくおさんバージョンアップのお知らせ(Ver1.2.0)
まくおさんをバージョンアップしましたのでお知らせします。
主な変更点は以下のとおりです。
【Ver1.1.3→Ver1.2.0】
- 応答速度と安定性の向上
- エラーログに"[error]"という文字列を追加
- メンバがタイムアウトしたときの状況を詳細に出力するようにした
- msync --statusで、実行中のコマンドを表示するようにした
- msyncを複数同時実行したときに挙動がおかしくなる不具合を修正
- ファイルクローズに失敗したときに異常終了する不具合を修正
- msyncにdeleteオプションを指定したときの結果表示がおかしくなる不具合を修正
- その他細かい不具合を修正
【ダウンロード】
【注意】Ver1.2系とVer1.1系はプロトコルに互換性がありませんので、
バージョンアップは全サーバに対して実施して下さい。
MAKUOSANバージョンアップのお知らせ(Ver1.1.2)
MAKUOSAN をバージョンアップしましたのでお知らせします。
主な変更点は以下のとおりです。
【Ver1.1.1→Ver1.1.2】
- シャットダウンメッセージを送出しない不具合を修正
- Ver1.1.1には、makuosanの終了通知が送出されないという不具合がありました。そのため、makuosanを終了しても、しばらくの間メンバーリストに残ってしまうという問題がありましたが、これを修正しました。
【Ver1.0.1→Ver1.1.1】
- msyncコマンドに--deleteオプションを追加
- 送信元に存在しないファイルを転送先で消すことができるようになりました。
- 転送速度が向上
-
ファイル転送速度が約1.5倍(当社比)になりました。
1対1での速度はまだscpにかないませんが、今後も随時改善していきます。
- エラーレポートを詳細に表示
- 転送先で発生したエラーなどを詳細に表示するようにしました。
【注意】
Ver1.0系とVer1.1系ではプロトコルに互換性がありません。
バージョンアップの際は全サーバに対して実施して下さい。
MAKUOSAN の詳細につきましてはプロ
ジェクトサイトを参照してください。
新機能の使用例
rsyncと同名のオプションなのでピンときた方もいると思いますが、 今回追加した--deleteオプションの使用例をご紹介します。 まずは、送信元サーバ(以下src)に適当にファイルを作ります。
src:~$ mkdir hoge src:~$ mkdir hoge/test1 src:~$ mkdir hoge/test2 src:~$ mkdir hoge/test3 src:~$ touch hoge/test1/aaa src:~$ touch hoge/test1/bbb src:~$ touch hoge/test1/ccc src:~$ touch hoge/test2/12345 src:~$ find hoge/ hoge/ hoge/test1 hoge/test1/aaa hoge/test1/bbb hoge/test1/ccc hoge/test2 hoge/test2/12345 hoge/test3
srcと転送先サーバ(以下dst)でmakuosanを起動します。
src:~$ makuosan -b ./
dst:~$ makuosan -b ./
srcからdstへhogeを転送します。
src:~$ msync -rv hoge update dst:hoge/test1/aaa update dst:hoge/test1/bbb update dst:hoge/test1/ccc update dst:hoge/test2/12345 update dst:hoge/test3 update dst:hoge/test1 update dst:hoge/test2 update dst:hogedstにhogeが転送されていることを確認します。
dst:~$ find hoge/ hoge/ hoge/test1 hoge/test1/ccc hoge/test1/aaa hoge/test1/bbb hoge/test2 hoge/test2/12345 hoge/test3
srcでhoge/test1を削除します。
src:~/work$ rm -fr hoge/test1 src:~/work$ find hoge hoge hoge/test2 hoge/test2/12345 hoge/test3
--deleteオプションを付けて再度転送します。
src:~$ msync -rv --delete hoge delete sag14:hoge/test1/ccc delete sag14:hoge/test1/aaa delete sag14:hoge/test1/bbb delete sag14:hoge/test1 update sag14:hoge
dstでもhoge/test1が消えています。
dst:~$ find hoge/ hoge/ hoge/test2 hoge/test2/12345 hoge/test3
shredのあれこれ
先日の shredを6倍速くしてみた のエントリは、 社内向けのメールをほとんどそのまま公開してしまったために、危険性にふれず安易に手抜きを推奨するような記事にしてしまいました。 社内でshredを使う目的は、過去に そのディスク、捨てる前に 〜shredで内容消去〜 でも紹介したように精神衛生上の問題なので乱数の質が落ちることは問題にならなかったのですが、これを機にもうすこしshredやHDDの消去について調べてみました。
shredの元になった論文
shredのinfoからリンクされている論文があります。 (Secure Deletion of Data from Magnetic and Solid-State Memory) この論文を読んでみると、shredがいくつものビットパターンで何passも書き込みを行う理由が説明されていました。
磁気的に消去するために、1と0を交互に何度も書き込みたいのですが、HDD(CDなども)の物理層で書き込まれるデータは、1や0が連続して出現しないように エンコーディングされていて(Run Length Limited (RLL))、奇数passで0xFFを、偶数passで 0x00を書き込むだけではフリップされないビットが存在するようです。 そこで、この論文では、どういったビットパターンを書き込めばRLLでエンコードされた物理層の全bitをフリップすることができるかをリストにしています。 ただし、現代のHDDがは単純なRLLよりも効率の良い(物理容量と論理容量の差が小さい)方式で書き込まれているので、RLLをターゲットにした ビットパターンで全bitをフリップできるとは限りません。その代わり、ランダムなデータを書き込むだけで、データを復元しにくくなります。
また、最初と最後にランダムを書き込むと少し、固定ビットパターンの書き込み順序をシャッフルするとさらに元のデータを推測しにくくなるとも かかれていました。
その他の参考文献
DoD 5220.22-M という規格があるらしく、それに準拠したという消去ツールがいくつかあります。 一次情報を探して見つけたのが DoD 5220.22-M です。この資料に書かれていた手順は、1. 全アドレス可能領域をあるcharで埋める、2. 補数で埋める、3. 乱数を書き込む、というものでした。
ただし、「DoD 5220.22-M準拠」という消去ツールでも消去時に書き込むデータはばらばらで、本当にこの資料のことを言っているのか自信がありません。
shredの過去の実装とML上での議論
shredが乱数を全面的に/dev/urandomに頼るようになったのはcoreutils-6.0からで、5系では、/dev/urandomから乱数の種だけ取ってきて、 乱数の生成は内部で行っていました。乱数生成には、ISAACという、暗号論的乱数生成アルゴリズムを用いていました。(stdlibのrandom()よりずっと 安全です)
どういう経緯なのかは判りませんが、coreutils-6.0からは 乱数をすべて /dev/urandom から持ってくるようになりました。 これで劇的に遅くなったのですが、MLで「5passで実行したら4日経っても終わらない」という報告がありました。 How to tune shred to run faster on Solaris 8
それに対する返信で、shredのデフォルトのパス数を変更しようという議論が行われたスレッド Default number of overwrites in shred が紹介されていました。 この議論では、現代のHDDなら1passでも十分効果があるとか、Secure Erase Newsletterでは2passと言われていたという発言もありました。
shred以外のツール
shredを /dev/sd? に対して利用する場合、書き込み対象は論理アドレスだけなので、代替セクタなどに情報が残る可能性があります。 そういった部分も消去するためには、HDD自身に消去機能が必要になるのですが、ATAコマンドに該当するコマンドがありました。Security Erase (SE) と Enhanced Security Erase (ESE) です。 ESEの場合、ユーザーに見えない領域も含めて、最低2passの乱数書き込みを行うようです。(参考: CMRR Secure Erase)
この機能をLinuxから使う場合、hdparm --security-erase-enhanced などとすれば良いらしいですが、Ubuntu Hardy で man hdparm してみたところ 「DANGEROUS」「EXPERIMENTAL」といった単語が並んでいました。代わりに、CMRRがHDDEraseというフリーソフトを出しています。 Ultimate Boot CD という多機能ブートCDの中にHDDEraseが入っているので、 自分のHDDがESEに対応している場合はこれを利用するとお手軽に消去ができそうです。
まとめ
いろいろ調べたわりに、あまりまとまった資料が見つけられなかったのですが、とりあえずまとめてみます。
まず、shredのデフォルトのpass数が25なのは、フロッピーや古いHDDのことも考えられているからで、現代のHDDに対してshredをかけるなら -n 2 か -n 3 で十分らしいです。
乱数の品質についてかかれた資料は見つかりませんでした。ただ、たくさんのpassを実行する中の一つのpassとして使う疑似乱数ならともかく、 pass数を減らして実行する場合は、乱数を予測されることで少しでもデータを復元できる可能性があがりそうです。 幸い、coreutils-5系を使うことで、stdlibのrandom()よりよっぽど安全な乱数を、/dev/urandomより ずっと高速に利用できることが判ったので、今後shredを利用するときには5系を使おうと思います。
その他の手段として、shredの--random-source で /dev/urandom の代わりに別のファイルを指定できるので、
$ mkfifo rnd $ openssl rand 999999999999999 > rnd & $ shred --random-source=rnd /dev/sdb
の用に、外部から乱数を流し込むこともできます。opensslの乱数生成は、/dev/urandomに比べてだいたい5倍ほど高速でした。
shredを6倍速くしてみた
HDDをshred(※)していて、おそいなー と思ったので、高速化してみました。
※shred: HDDにランダムな値を書いたり特定のビットパターン書いたりして、データを復元し難くするツール
shred は -n でパス数を指定できて、-n 3くらいにすると3パスともランダムなデータの書き込みになります。 coreutils-6.9では、デフォルトでこの乱数は/dev/urandomからとってきているのですが、非常に遅いです。
試しにddで/dev/urandomの速度を計ってみると、
root:~# dd if=/dev/zero of=/dev/null bs=1M count=128 128+0 records in 128+0 records out 134217728 bytes (134 MB) copied, 0.0409313 s, 3.3 GB/s root:~# dd if=/dev/urandom of=/dev/null bs=1M count=128 128+0 records in 128+0 records out 134217728 bytes (134 MB) copied, 38.0659 s, 3.5 MB/s
ということで、1000倍くらい遅いです。 (Core Duo 1.66GHzな個人PCで Ubuntu 8.04)
で、coreutils-6.9のソース持ってきて、バッファに乱数を用意している部分を見つけて、 ちくっと書き換えてみました。
coreutils-6.9/src/shred.c 430行目から #if 0 /* original */ randread (s, &r, lim); #else /* use random() for speedup. */ unsigned int seed; int i; randread (s, &seed, sizeof(seed)); srandom (seed); assert ((lim % 2) == 0); for (i = 0; i < lim; i+=2) { int r16 = random(); r.u[i] = r16; r.u[i+1] = r16 >> 8; } #endif
このrandreadというのが、乱数のソース s(デフォルトでは/dev/urandom)から、lim byteの データを取得して、r に書き出します。
で、乱数の種だけ/dev/urandomからとってきたら、実際に書き込むのは疑似乱数でいいよねーって ことで実装したのが #else 以降です。
とりあえず、改造前と改造後のshredの結果が以降になります。
root:~# time /usr/bin/shred -n 3 -v -z -s 256M /dev/sdb #改良前 /usr/bin/shred: /dev/sdb: 経過 1/4 (random)... /usr/bin/shred: /dev/sdb: pass 1/4 (random)...14MiB/256MiB 5% /usr/bin/shred: /dev/sdb: pass 1/4 (random)...29MiB/256MiB 11% /usr/bin/shred: /dev/sdb: pass 1/4 (random)...43MiB/256MiB 17% ... snip ... /usr/bin/shred: /dev/sdb: 経過 4/4 (000000)... real 4m26.913s user 0m0.024s sys 3m52.167s root:~# time /home/methane/local/bin/shred -n 3 -v -z -s 256M /dev/sdb # 改良後 /home/methane/local/bin/shred: /dev/sdb: 経過 1/4 (random)... /home/methane/local/bin/shred: /dev/sdb: pass 1/4 (random)...253MiB/256MiB 98% /home/methane/local/bin/shred: /dev/sdb: pass 1/4 (random)...254MiB/256MiB 99% /home/methane/local/bin/shred: /dev/sdb: pass 1/4 (random)...256MiB/256MiB 100% /home/methane/local/bin/shred: /dev/sdb: 経過 2/4 (random)... /home/methane/local/bin/shred: /dev/sdb: pass 2/4 (random)...237MiB/256MiB 92% /home/methane/local/bin/shred: /dev/sdb: pass 2/4 (random)...238MiB/256MiB 92% /home/methane/local/bin/shred: /dev/sdb: pass 2/4 (random)...256MiB/256MiB 100% /home/methane/local/bin/shred: /dev/sdb: 経過 3/4 (random)... /home/methane/local/bin/shred: /dev/sdb: 経過 4/4 (000000)... real 0m43.516s user 0m14.017s sys 0m1.400s
256MBをshredが、4:26 から 43sec に高速化しました
あと、user+sys時間にも注目してください。改造前はすごくCPU喰ってたんですが、 改造後はCPU使用率が低くなりました。改造前はCPUバウンドだったのが、改造後は 十分速くなってIOバウンドになったんですね。なので、もっと高速なHDDであれば、 速度差はもっと広がるかもしれません。
shredの遅さにお悩みの方はお試しください。(当然ながら、上記修正を施したshredを使って発生したいかなる損害についても、
私もKLabもまったく責任を負いません)
(下の追記をご覧ください)
coreutilsのビルド方法についてですが、GNUのサイトからソースをダウンロードしてきて、
./configure --prefix=$HOME/local make
・・・するとビルドエラーになりました。(Debian etchとUbuntuでそれぞれ違う場所でエラーになっていました)
とりあえず、コンパイルエラーになっているのはshredじゃないので、
cd src make shred
すると、shredのバイナリができました。
余談になりますが、coreutilsの中にはbase64とかmd5とかshaとかregexが実装されています。 ソース一式手元に置いておくと、何か使いたいときにすぐに持ってこられて便利かもしれません。
追記
コメントでご指摘頂いたとおり、この改造ではランダムビットパターンの品質が低下します。安易に薦めて良い改造ではありませんでした。申し訳ありません。
私はshredでどんなデータを何回書いたら元のデータを復元できる確率がどれだけあるのかをまったく知らず、単に精神衛生上の問題でshredを使用しているだけなので、この様な改造をしました。この改造でどれほど危険性が上がるのかは理解していません。
私の用に精神衛生上の理由でshredを使っていて、安全性と速度を比較した上で、この改造を受け入れられる方に限り、ご利用ください。
なお、上記の改造では、random()を6×1024回呼び出す毎に、/dev/urandomから取得した乱数の種をsrandom()で設定しています。安全性を上げるためには、
- もっと品質の高い疑似乱数関数を利用する
- 乱数の種を設定する頻度を上げる
- 乱数の種を設定する周期を、/dev/[u]random を元に決定する
といった事が出きるかもしれません。
もし、shredよりも速くて、信頼性のありそうな累次のツールがありましたら、コメント欄で教えていただけると幸いです。
カラフル端末で視認性を高める - ls編
色気はさておき、逆説的ですがモノトーンのターミナルで色を使うと、視認性がグッと上がります。
というわけで、今回から数回にわけて、ターミナルの中で色を効果的に使い、いかに色気を出すか、もとい、視認性を高める方法を紹介します。
第1回は ls (GNU coreutilsのls-5.2.1) を取り上げます。
$ ls -F
broken-symlink@ directory/ file file.orig write-blog.todo
ddl.sql executable* file.bak symlink@
続きを読む
そのディスク、捨てる前に 〜shredで内容消去〜
DSASはサーバが200台近くあり、複数台ディスクを積んでいるサーバもあるのでディスクの数はそれ以上です。これだけディスクがあると、どれかが壊れる確率はそれなりに高くなります。
ちなみにDSASは、
- Webサーバは数十台あってディスク内容は全サーバで同期している。
- DBサーバのデータ格納用ディスクはRAIDを使っているし、レプリケーションもしている。
- LVSを使っている負荷分散機などいくつかのサーバはネットブートでディスクレスにしている。(のでディスク故障とは無縁)
というふうに、ディスクが数台壊れたぐらいではサービス停止することのない構成になってます。
でも、壊れたディスクはデータが入っているのでそのままゴミ箱にポイというわけにはいきません。
rm -frで消したりfdiskでパーティションテーブルを壊したとしても、いくらでもディスク上のデータを取り出す方法はあります。例えば、ddでデバイスを吸い出すとか。
そこで、今回は廃棄や保守交換で返送する前にshredをかけましょうというお話をしたいと思います。
続きを読む