仙石浩明の日記

システム構築・運用

2011年3月7日

Osukiniサーバを使ってみた 〜 仮想コンソールが提供されない VPS で独自OS をインストールする方法 hatena_b

今まで米国の VPS を使っていたが、 いつのまにか日本の VPS もリーズナブルな値段になってきたらしい。 「さくらのVPS」 がメモリ 512MB で月額 980円、 「Osukini サーバ」 ST プラン がメモリ 1GB, ディスク 100GB で月額 980円。 「さくらのVPS」 でも充分安いが、 「Osukini サーバ」 の安さは目を疑うレベル。 これはもう試してみるしかない。

実は、 最初は 「さくらのVPS」 を申し込んだのだが、 rootパスワードが見つからない事件でイカってキャンセルしてしまった。 申し込んだ直後に VPS が起動できたので、 さくら++ と思いつつ root でログインしようとしたらパスワードが分からず Web じゅう探しまわる羽目に。 万策尽きてサポートに電話したら 15分以上待たされ、 しかも担当者には root と言っても話が通じず、 別の番号にかけなおせと言われる始末。 後ほど届いた 「仮登録完了のお知らせ」 メールに root パスワードが書いてあったのだが、 メールが届く前に VPS のコントロールパネルをいじれて、 起動までできてしまう現在の仕様はいかがなものか。

Osukini サーバの場合 OS は CentOS, Ubuntu, Debian から選べるが、 「my distribution」 以外は使う気が起きないので、 どうやって私の 「独自OS」 をインストールするか、 方法を考えてみた。 もちろん、 Osukini サーバのサポートの範囲外なので、 インストールする方法だけでなく、 トラブった時の復旧方法を考えておくことが重要。

Osukini サーバでは 仮想コンソール (hvc console, リモートコンソール) が提供されない。 OS の外部から操作する手段がほとんどなく、 ブートスクリプトの設定ミスなどで OS にリモートログインできなくなってしまったら最後、 OS の再インストールしか復旧手段はない (と思う)。 しかも、 (サポートの範囲外である) 独自OS の再インストールを支援してもらえるはずはないので、 せっかくインストールした独自OS は跡形もなく消えてしまう。

再起動すれば復旧できる一時的な障害はここでは考えない。 設定ファイルなどを不適切な内容に書き換えてしまった結果、 再起動してもリモートログインできない状態が続くケースのみを考える。 実際、 サーバを運用しているとそういうトラブルはよくある。 ネットワーク設定を間違えて通信不能になるだけで、 他は全く正常でもリモートログインはできなくなるし、 あるいは sshd の設定を間違えて sshd が起動しなくなっただけでもリモートログインできなくなる。
ネットワークが全く使えなくても、 sshd が立ち上がらなくなってしまっても、 OS 自体が正常に起動していれば init から起動される getty は生きていることが多いので、 仮想コンソールさえ使えれば、 大抵はログインできて修復できる。

他の VPS, 例えば Linodeprgmr などでは、 仮想コンソールが利用できる。 さらに、 両者とも GRUB が使えるので、 複数 OS をインストールしておいて切り替えて使うこと (いわゆる 「デュアルブート」) ができる。 つまり一つの OS 上で何かトラブルが起きて OS が立ち上げ不能になっても、 別の OS (いわゆるレスキュー用 OS) を立ち上げて、 トラブった OS を修復することができる。

リアルサーバでは、 (仮想ではなく物理的な) コンソールが利用できる (当たり前)。 さらに、 トラブった時はレスキュー用の CD-ROM / USBメモリからブートして復旧作業を行なったりする。 VPS でもリアルサーバでも、 イザというときに備えて、 通常使う OS とは別に、 修復作業を行なうためのレスキュー用の OS を用意しておくことが極めて重要。

ところが、 レスキュー用 OS が一切利用できないばかりか、 仮想コンソールから操作することすらできないのが Osukiniサーバ。 トラブルに対して極めて脆弱であると言わざるを得ない。

Osukini Server Control Panel

VPS にとって仮想コンソールは必須の機能だと思っていたので、 Osukiniサーバのコントロールパネルを見て驚いた。 「再起動」 「停止」 などの操作と、 OS の初期化しかできない (値段相応? でも、 仮想コンソール機能は Xen 標準なのに...)。

OS の初期化をすれば、 全てが消えてしまう。 いわば、 クリーンインストールしかできない CD-ROM からブートするようなもの。 トラブったからといって、 即クリーンインストールしたいという人がどれだけいると言うのか?

嘆いていても仕方がないので、 どうやってレスキュー用 OS 相当のことを実現するか考えてみる。 レスキュー用 OS を立ち上げることさえできれば、 独自 OS をインストールすることも可能になる。 こんなこともあろうかと、 2年前 (2008年10月) に作っておいた ptyd (Pseudo TTY Daemon) が役に立ちそう。

More...
Filed under: システム構築・運用 — hiroaki_sengoku @ 08:36
2010年6月17日

Linode から prgmr.com へ乗り換えてみた hatena_b

今年 2月から使っている Linode (VPS, 仮想サーバ) は、 月額 $19.95 の最小プラン Linode 360 だと RAM 360MB なので WordPress を動かす (Apache+PHP+MySQL) には若干足らない (稀に スラッシング 状態に陥って無反応になる)。 もちろん、 一つ上のプラン Linode 540 (RAM 540MB) にすればいいのであるが、 これだと月額 $29.95 でちょっと高い (私の感覚だと月額 $20 あたりに心理的な壁がある)。

prgmr.com ならば、 RAM 512MB, ディスク 12GB が月額 $12 (年一括払いなら $115.2) で済むので乗り換えてみた。 Linode のようなユーザフレンドリーな Web ユーザインタフェースは無いし、 時々新規申込みの受付を中止したりする (今も RAM 1024MB 以上のプランは受付けていない) が、 逆に言えば Web インタフェースが不要な人 (含む私) にとっては prgmr.com のシンプルさがむしろ望ましく、 レスキュー用のブートイメージなど必要最低限のものは揃っているので、 不便と感じることはない。 また、 いったん申込みが完了してしまえば受付の一時停止は関係ないわけで、 むしろサーバリソースに見合ったユーザ数しか受付けない姿勢は好感が持てる。

Web から Signup すると、 以下のようなメールが届く。

you ordered a xen vps, 512MiB ram, 12GiB Disk 80 GiB transfer Xen VPS,  $12/month username sengoku 

Before I can set you up, I need to know what distro you would like and an OpenSSH format public key (on a *NIX, run ssh-keygen  and send me either the id_dsa.pub or id_rsa.pub file in an attachment)    

on putty/windows, see
http://www.unixwiz.net/techtips/putty-openssh.html#keypair
and scroll down to the screen shot where it says 'openssh format public key for pasting into authorized_keys' - that is what I need.

thanks. 

OpenSSH の公開鍵くらい Web から登録させればいいのにと思いつつ、 「I need to know what distro you would like」 などと聞いてくるということは多少の融通は聞くのかと思い、 パーティションの切り方をついでにリクエストしてみた (6/4 15:46)。 つまり、 自分で作った Linux distro (配布していないから 「distribution」 と呼ぶのは正確性に欠けるのだが、 それなら何て呼んだらいいのだろう?) をインストールしたいから、 最小のパーティション (例えば 2GB) に ubuntu をインストールしておいてもらえるか? などとリクエストしてみた:

Hi,

At Fri, 04 Jun 2010 05:48:05 +0000,
support@prgmr.com wrote:

> Before I can set you up, I need to know what distro you would like and
> an OpenSSH format public key (on a *NIX, run ssh-keygen and send me
> either the id_dsa.pub or id_rsa.pub file in an attachment)

I'd like to use my own linux-based image that is running on
fremont.gcd.org (linode) now.  Is it possible to make two partitions
(plus swap partition) ?  One is for a normal ubuntu, and the other is
for my own image.  I'd like to switch these two OSs by pv-grub.

Please minimize the ubuntu partition in order to maximize my own image.
For example, 2GiB for ubuntu, 1GiB for swap, and 9GiB for me.

I've attached my public key id_dsa.pub at the end of this mail.


#9860.
https://www.gcd.org/sengoku/                Hiroaki Sengoku <sengoku@gcd.org>

すると、 4日後の 6/8 09:03 に返事が来た。 リクエストはあっさり無視されて、 ディスク一杯に ubuntu をインストールした状態の VPS が提供されただけ。 個別対応しないのであれば、 自動化したほうがいいような。

Your vps is setup now, your host server is whetstone.prgmr.com.
Login with your username and ssh key and follow the instructions
at http://book.xen.prgmr.com/mediawiki/index.php/Quickstart
For more information about repartitioning, see
http://book.xen.prgmr.com/mediawiki/index.php/Repartition
The default root password for the vps is password, and you
should receive a bill soon by email. If you have any questions
please email support@prgmr.com. Thanks very much.
Nick Schmalenberger

言われた通り ssh でログインしてみる:

senri:/home/sengoku % ssh whetstone.prgmr.com
The authenticity of host 'whetstone.prgmr.com (65.49.73.105)' can't be established.
RSA key fingerprint is 97:52:07:d4:52:8d:c0:cc:13:c7:fb:5d:5d:1b:ab:b4.
Are you sure you want to continue connecting (yes/no)? yes
Warning: Permanently added 'whetstone.prgmr.com,65.49.73.105' (RSA) to the list
of known hosts.
Name                                        ID   Mem VCPUs      State   Time(s)
sengoku                                    xxx   512     1     -b----     11.5

Options for sengoku
1. console
2. create/start
3. shutdown
4. destroy/hard shutdown
5. reboot
6. swap i386/amd64 bootloaders (pvgrub)
7. exit
press the number> 

「6. swap i386/amd64 bootloaders (pvgrub)」 が目を引くが、 PV-GRUB は DomainU 側で実行されるので、 PV-GRUB にて 32bit/64bit の両方のカーネルを立ち上げることができず、 PV-GRUB を実行する前にどちらか一方に決めておく必要がある、 ということのようだ。 デフォルトは (残念なことに) 32bit になっていて、 セットアップしてもらった ubuntu も 32bit 版だった。

「1」 を入力して VPS のコンソールを表示させてみる:

press the number> 1

Ubuntu 10.04 LTS sengoku.xen.prgmr.com hvc0

sengoku.xen.prgmr.com login: root
Password:
Last login: Mon May 31 23:51:42 UTC 2010 on hvc0
Linux sengoku.xen.prgmr.com 2.6.32-22-generic-pae #33-Ubuntu SMP Wed Apr 28 14:57:29 UTC 2010 i686 GNU/Linux
Ubuntu 10.04 LTS

Welcome to Ubuntu!
 * Documentation:  https://help.ubuntu.com/
root@sengoku:~# df
Filesystem           1K-blocks      Used Available Use% Mounted on
/dev/xvda1            11812024   1068124  10143876  10% /
none                    249488       136    249352   1% /dev
none                    253744         0    253744   0% /dev/shm
none                    253744        28    253716   1% /var/run
none                    253744         0    253744   0% /var/lock
none                    253744         0    253744   0% /lib/init/rw
none                  11812024   1068124  10143876  10% /var/lib/ureadahead/debugfs
root@sengoku:~# 

前掲のメールに書いてあった通り、 ユーザ 「root」 パスワード 「password」 でログインできた。 初めてログインしたのに 「Last login: Mon May 31 23:51:42 UTC 2010 on hvc0」 などと出ているが、 この ubuntu イメージを作ったとき試しに root でログインしたのが残ってしまったのだと思われる。 新規ユーザのセットアップのたびに、 5/31 に作ったイメージを使いまわしているのだろう。

ディスク 12GB のプランのはずなのに 1GB ほど足りないのは swap パーティションがあるから?と思い、 fdisk で確認してみる:

root@sengoku:~# fdisk -l /dev/xvda

Disk /dev/xvda: 12.8 GB, 12884901888 bytes
255 heads, 63 sectors/track, 1566 cylinders
Units = cylinders of 16065 * 512 = 8225280 bytes

    Device Boot      Start         End      Blocks   Id  System
/dev/xvda1               1        1494    12000523+  83  Linux
root@sengoku:~# 

シリンダの最大値は 1566 なので、 72 (=1566-1494) シリンダが使われずに余っている。 試しにパーティションを割当ててみたら普通に使えた。

自前 distro 10GB, swap 1GB, ubuntu 1GB といった切り分けかたにするため、 レスキュー用のカーネル 「CentOS 5.5 rescue」 をブートしてみる:

    GNU GRUB  version 0.97  (65536K lower / 0K upper memory)

 +-------------------------------------------------------------------------+
 | user bootloader configuration                                           |  
 | CentOS 5.5 rescue (2.6.18-194.3.1.el5xen)                               |
 | CentOS 5.5 installer                                                    |
 | NetBSD 5.0.2 installer                                                  |
 | Ubuntu 10.04 LTS installer                                              |
 |                                                                         |
 |                                                                         |
 |                                                                         |
 |                                                                         |
 |                                                                         |
 |                                                                         |
 |                                                                         |  
 +-------------------------------------------------------------------------+
    Use the ^ and v keys to select which entry is highlighted.
    Press enter to boot the selected OS, 'e' to edit the
    commands before booting, or 'c' for a command-line.

二行目の 「CentOS 5.5 rescue (2.6.18-194.3.1.el5xen)」 を選択してブート。 root でログイン (パスワードは無し) して fdisk を使ってパーティションを切り直した。 せっかくセットアップしてもらった Ubuntu も、 3分と使わずにお別れ。

CentOS release 5.5 (Final)
Kernel 2.6.18-194.3.1.el5xen on an x86_64

sengoku.xen.prgmr.com login: root
[root@sengoku ~]# 
        ...
[root@sengoku ~]# fdisk -l /dev/xvda

Disk /dev/xvda: 12.8 GB, 12884901888 bytes
255 heads, 63 sectors/track, 1566 cylinders
Units = cylinders of 16065 * 512 = 8225280 bytes

    Device Boot      Start         End      Blocks   Id  System
/dev/xvda1               1        1306    10490413+  83  Linux
/dev/xvda2            1307        1438     1060290   82  Linux swap / Solaris
/dev/xvda3            1439        1566     1028160   83  Linux
[root@sengoku ~]# mount /dev/xvda1 /mnt
[root@sengoku ~]# 

私は個人で管理している Linux マシン (自宅と職場と VPS 合わせて十数台, もちろん商用サービスを行なっているサーバではない) のディスクの内容を、 コマンド一発で同一に保てるようにしている (つまりマスタで変更/追加/削除したファイルを各マシンへコピー/各マシンで削除)。

もちろん、 各マシンの性能や 32bit/64bit などの CPU 種別、 ディスクの容量に応じて違う部分もあるわけで、 この同期コマンドは同期先の各マシンに合わせてコピーしないファイルのリストを生成した上で rsync を実行している。 なので、 新しくマシンを増やすときもマスタ上で同期コマンドを実行するだけで、 インストールが完了するはずだが...

More...
Filed under: システム構築・運用 — hiroaki_sengoku @ 09:44
2010年4月20日

Linux サーバ同士をシリアルケーブルでつなぐには寡黙な getty が必要 hatena_b

私の自宅 LAN (GCD) の対外ルータである 2台の Linux サーバ (senriasao) は、 双方のシリアルポート (つまり DSUB9ピンの RS-232C) 同士がつながっている。 いまさら RS-232C ? と思う人が大半だと思うが、 最後の命綱としての安心感は測り知れない。

つまりサーバをリモートから (ネットワーク経由で ssh ログインして) いじっていると、 ちょっとしたミスで操作不能に陥ってしまうことがある。 例えば NIC を down してしまった、 iptables で DROP する設定にしてしまった、 sshd を殺してしまった、 等々。 あるいは、 リモートから再起動を行なう場合、 起動スクリプトのほんの些細な設定ミスで、 リモートからは操作不能になってしまう。

こんなとき、 2台のサーバのシリアルポート同士を RS-232C クロスケーブル (シリアルケーブル) で接続しておけば、 生きている方のサーバからシリアルケーブル経由でログインできて、 リモートからは操作不能に陥ったサーバのネットワーク設定を修復したり、 sshd を起動し直したり、 あるいは再起動の場合であれば、 GRUB を操作することができる (ML115 などであれば BIOS 設定までいじれる)。

職場のサーバ群は、 もちろん IPMI を利用していて大抵の操作は IPMI でできてしまうのでシリアルケーブルの出番はあまりないのだが、 それでもやはり最後の命綱がある安心感は何ものにも代えがたい。 しかも数百円のケーブル一本で済むのだから、 掛けておくべき保険だろう。

シリアルケーブル経由でサーバにログインするには、 そのサーバの /dev/ttyS0 (シリアルポート COM1 の場合) で getty を動かしておく必要がある。 例えば GCD の場合 mgetty を使っているので、 /etc/inittab に

# Dialin lines
s0:25:respawn:/usr/sbin/mgetty ttyS0

などと設定している。 シリアルケーブルでつながった 2台の Linux サーバ senri と asao において、 asao から senri へシリアルケーブル経由でアクセスすると、 senri のシリアルコンソールが表示される:

asao:/home/sengoku % cu ttyS0
Connected.

Welcome to Linux 2.6.31.13-x86_64.


senri.gcd.org!login: root
otp-md5 299 se5047 ext
Response or Password:
Last login: (null) on /dev/ttyS0

senri.gcd.org:/root # tty
/dev/ttyS0
senri.gcd.org:/root # 

たとえ senri のネットワーク機能が全滅していたとしても、 mgetty は /sbin/init から直接起動されるので影響を受けない。 シリアルコンソールから root でログインして復旧作業を行なうことができる。 逆に、 asao のネットワーク機能が全滅したときは、 senri へ ssh ログインした上で asao へシリアルケーブル経由でアクセスすればよい。 ネットワーク機能が麻痺する可能性があるような危険な作業を senri と asao の両方で同時に行なったりしないようにすれば、 何が起きてもどちらかは生きていることが保証できる。

さて、 両サーバで mgetty が動いているときは以上で問題無いのだが、 どちらかのサーバを再起動するときなど、 mgetty 以外のものが動くときは状況が変わってくる。

More...
Filed under: システム構築・運用 — hiroaki_sengoku @ 07:48
2010年2月25日

本ブログのサーバを海外レンタルサーバ Linode を使って冗長化・地域分散してみた hatena_b

前回前々回に書いたように、 このブログ 「仙石浩明の日記」 (および 「仙石浩明CTO の日記」) は、 私の自宅にある PC サーバで動かしている。 2台の PC からなる冗長構成になっていて、 10年以上の安定稼働実績がある (両サーバ共に停止したことは皆無) が、 回線が (家庭用の) フレッツ光ネクストなので、 インターネットとの接続が切れてしまう可能性は皆無ではない。 実際、 Bフレッツからフレッツ光ネクストへ切り替えたときは 40分間ほど切れてしまった。 一応バックアップ回線として ADSL 回線も契約しているが、 こちらは固定 IP アドレスを割当てていないので WWW サービス用としては使いにくい。

短時間とはいえブログが見えないようなことがあると、 たまたまそのタイミングで (検索エンジン等で見つけて) 訪れた人が、 ブログが既に閉鎖してしまったものと勘違いしてしまうかもしれない。 ここは一つ自宅以外の場所にもサーバを立てて、 回線断の影響を受けないようにしたいところ。 海外のサーバなら地域分散もできてなおよい。

道楽で運営している自宅サーバにそこまで可用性を求めるのは、 やりすぎの感もあるが、 近頃流行りの VPS (Virtual Private Server, 仮想専有サーバ) は、 月額 $20 (初期費用無し) 程度で利用できるらしい。 $20 なら試しに使ってみるのも悪くない。

サービス
プラン名
メモリ
ディスク
帯域
追加額/単位
月額
年額
データセンタの
場所
ServerAxis Xen
VS000.5G-0025.0GP
512MB
25GB
0GB
$.05/GB
$15
$180
Chicago, IL
Linode
Xen 360
360MB
16GB
200GB
$10/100GB
$19.95
$215.46
Fremont, CA
Dallas, TX ...
VPSLink
Xen VPS Link-3
256MB
10GB
300GB
$.50/GB
$19.95
$201.12
Seattle, WA
New York, NY
slicehost
Xen VPS 256 slice
256MB
10GB
150GB
$.30/GB
$20
$216
Dallas, TX
St. Louis, MO

VPSLink 以外は最安のプランで比較した。 VPSLink には、 メモリ 64MB で月額 $7.95 の Link-1 プラン、 メモリ 128MB で月額 $13.95 の Link-2 プランもあるが、 256MB 未満のメモリではできることも限られてしまうので比較対象から外した。

月額基本料だけを比較すると、 ServerAxis が格安に見えるが、 これは帯域課金が含まれていないためで、 例えば 100GB (送受信の合計バイト数) 使うと $5 加算されて $20 と他社並になる。

データセンタの場所 (というかネットワーク上での位置) によって遅延時間が変わってくるので、 どこのデータセンタを使っているかも重要。 例えば同じ Linode でもデータセンタによって、 日本からアクセスしたときの RTT (Round Trip Time, 往復にかかる時間, ミリ秒) が倍以上変わってくる。 それぞれ 10回ずつ ping を打って、 RTT の最小/平均/最大を比較してみる:

データセンタの場所ホスト名 最小平均最大
Fremont, Californiafremont1.linode.com 121.341132.299146.939
Dallas, Texasdallas1.linode.com 171.049173.152174.905
Atlanta, Georgiaatlanta1.linode.com 183.184185.938190.174
Newark, New Jerseynewark1.linode.com 200.629203.053204.755
London, Englandlondon1.linode.com 269.324273.499276.822

米国西海岸だと RTT は 130msec 程度で済むが、 東海岸は 200msec を超えてしまい、 英国はもっと遠い。 西海岸にデータセンタがあるのは Linode と VPSLink だが、 Linode がデータセンタの情報を詳細に公開しているのに対し、 VPSLink は、 Spry Hosting のシアトルのデータセンタを使用している、 ということ以上の情報を公開していない (しかもこのページは 2006年8月の内容のまま) のが少し気になる。

サポートしている Linux ディストリビューションは、 各社だいたい共通で、 CentOS, Debian, Gentoo, Ubuntu の各バージョンが利用できるが、 VPSLink Ubuntu Plan のページに Ubuntu 9.10 Karmic が書かれていない点も、 ちょっと気になった (ちゃんとポリシーを持って 9.10 をサポートしないのならいいのだが、 どうもそうではなく単に運用の手を抜いているだけのような気配がする)。 Web 界隈での評判も Linode のほうがよさげだったので、 Linode を使ってみることに決めた。

サポートしているディストリビューションのバージョンが古くてもアップグレードできるのだろうが、 帯域で課金されるので最初から新しいバージョンをインストールできる方が好ましい。

サービス CentOSDebianGentooUbuntu Fedoraその他
ServerAxis 5.45.02008.09.10- Mandriva SUSE
Linode 5.35.02008.09.1011 Arch SUSE Slack
VPSLink 55O9.0411 Arch Slack
slicehost 5.45.02008.09.1012 Arch RedHat

おそらく各社ともカーネルも入れ替えることができるのだと思うが、 Linode 以外は使っていないので未確認。 Linode の場合は pv_ops (paravirtualization) を有効にしておけば任意のカーネルを利用できる。 したがって (帯域課金さえ気にしなければ ;-) 任意のディストリビューションの利用 (あるいは完全なカスタマイズ) が可能 (私はまだそこまではやっていない)。

なお、VPS サービスは探すといろいろあるようだ。 前述した 4社と比べると公開している情報が不十分 (特に Virtuozzo/OpenVZ な VPS は実際のパフォーマンスを予想しにくく、 実地に使ってみないとなんとも...) なので私は比較検討対象から外したが、 ダメ元で使ってみるのも面白いかもしれない:

サービス
プラン名
メモリ
ディスク
帯域
追加額/単位
月額
年額
データセンタの
場所
SplitServ
Xen 1024
1024MB
15GB
200GBs
$8/400GB
$14.95
$149.50
Los Angeles, CA
Kansas City, MO
WebKeepers
Virtuozzo ライト
512MB
50GB
?1280円
11760円
?
PhotonVPS
Xen VPS WARP 1
512MB
35GB
500GB
?
$16.95
$203.40
Los Angeles, CA
KnownHost
Virtuozzo VS2
512MB
30GB
750GB
$40/100GB
$35
$400
Los Angeles, CA
Dallas, TX
BurstNet
VPS PACKAGE #1
512MB
20GB
1000GB
$25/200GB
$5.95
$71.40
Scranton, PA
VPS NOC
OpenVZ Bronze
512 MB
20 GB
400 GB
?
$12.95
$155.40
Kansas City, MO
CoreNetworks
CoreMR
512MB
12GB
500GB
?
$19.95
$239.40
?
prgmr.com
Xen VPS
512MB
12GB
80GB
?
$12
$115.20
San Jose, CA
HostCadet
VPS Micro
512MB
10GB
400GB
?
$9.99
$119.88
Colorado Springs, CO
NY NOC OpenVZ
Super VPS #1
512MB
10GB
1000GB
?
$10.00
$120
Chicago, IL
North Bergen, NJ
ARP Networks
KVM/QEMU
512MB
10GB
100GB
?
$15
$180
Los Angeles, CA
VirtuallyDedicated
Xen VX384
384MB
24GB
300GB
?
$10.99
$131.88
Chicago, IL
Scranton, PA
GrokThis.net
Xen VPS
320MB
16GB
160GB
?
$20
$200
Philadelphia, PA
DMEHosting
OpenVZ VPS1
256MB
25GB
1000GB
$10/200GB
$5.95
$71.40
Denver, CO
Chicago, IL
Quantact
OpenVZ VS1
256MB
15GB
300GB
$1/GB
$14.99
$179.88
Santa Rosa, CA
RackspaceCloud
Xen CloudServers
256MB
10GB
0GB
$.08-$.22/GB
$10.95
$131.40
Dallas, TX
XENnode
XEN 256
256MB
10GB
200GB
?
$14.99
$143.99
Dallas, TX
Datarealm Xen
PowerVPS
256MB
10GB
1Mbps
Unmetered
$19.95
$215.52
?
HostVirtual Xen
XV0 Server
256MB
10GB
250GB
?
$14.95
$179.40
San Jose, CA
RackUnlimited
Xen RVPS-Starter
256MB
10GB
100GB
?
$8
$96
Asheville, NC
QuickWeb
Xen Lite
256MB
5GB
240GB
?
$17.95
$215.40
San Jose, CA
QuickVPS
Xen プラン1
256MB
10GB
50GB
?
2300円
21600円
日本国内
QuillHost
OpenVZ Standard
256MB
8GB
150GB
?
$9.99
$119.88
Washington, DC
Charlotte, NC

と、いうわけで、 Linode 360 を契約してみた。

More...
Filed under: システム構築・運用 — hiroaki_sengoku @ 08:58
2010年2月10日

CTO日記も livedoorブログから WordPress へ引越しました (URL は変更なし)

「仙石浩明の日記」 に続いて、 「仙石浩明CTO の日記」 も先週末に livedoorブログから自宅サーバへ引っ越した (つまりネームサーバの設定を変更して切替。有料プランの解約はこれから)。 もともと両ブログは相互にリンクを張って密接に連係していたので、 引越を機会に両者を統合した。

統合といっても両ブログは微妙(?)に読者層が異なると思われるし、 何よりページの体裁が大きく変わってしまっては読者の方々を戸惑わせてしまうので、 CTO日記を 「仙石浩明の日記」 の一カテゴリという位置付けにして、 かつページの体裁は WordPress のテーマを切り替えることによって、 どちらのブログもあまり大きな変化がないようにしている。

「仙石浩明CTO の日記」 http://sengoku.blog.klab.org/ をアクセスすると、 次のような PHP スクリプトを走らせた上で、 WordPress を呼び出す (末尾の require 文):

<?php
$new = NULL;
if ($_SERVER['REQUEST_URI'] == "/") {
    $new = "/blog/category/cto/";
} elseif ($_SERVER['REQUEST_URI'] == "/feed/") {
    $new = "/blog/category/cto/feed/";
} elseif (preg_match('@^/\d+/\d+/\d+/@',
                     $_SERVER['REQUEST_URI'], $matches)) {
    $new = $_SERVER['REQUEST_URI'];
...(中略)...
}
if ($new) {
    ...(中略)...
    $ORIG_SERVER_NAME = $_SERVER['SERVER_NAME'];
    $host = "www.gcd.org";
    $_SERVER['SERVER_NAME'] = $host;
    $_SERVER['REQUEST_URI'] = $new;
    $_SERVER['SCRIPT_NAME'] = $new;
    $_SERVER['PHP_SELF'] = $new;
    $abspath = "/usr/local/www/wordpress/";
    $themepath = "${abspath}wp-content/themes/sengoku_cto/";
    define('WP_USE_THEMES', true);
    define('TEMPLATEPATH', $themepath);
    define('STYLESHEETPATH', $themepath);
    require("${abspath}wp-blog-header.php");
...(中略)...
}
?>

つまり http://sengoku.blog.klab.org/ へのアクセスは、 パス名に 「/category/cto/」 を追加することによって、 CTO日記カテゴリへのアクセスに変換する。

ページの体裁については、 「wp-content/themes/sengoku_cto/」 ディレクトリが、 CTO日記のテーマフォルダで、 二つの PHP 定数 TEMPLATEPATH と STYLESHEETPATH をこのディレクトリへ設定することによって、 テーマの切り替えを行なっている。

テーマフォルダの中にあるテーマ関数ファイル 「functions.php」 は、 WordPress の初期化中に読み込まれるので、 ここに PHP スクリプトを書いておくことによって WordPress の挙動を変更することができる。 例えばブログのタイトルを 「仙石浩明CTO の日記」 に変更するには、 以下のスクリプトを functions.php に追加しておけばよい:

function option_blogname_cto() {
    return '仙石浩明CTO の日記';
}
add_filter('pre_option_blogname', 'option_blogname_cto');

つまり、 pre_option_blogname フックに、 option_blogname_cto フィルタを登録する。

WordPress では、 ブログのタイトルなど各種オプションの設定値 (DB に格納している) を、 get_option($setting) 関数を呼び出すことで参照している。 例えばタイトルは get_option('blogname') を呼び出すことで得られ、 URL は get_option('home') で得られる。

get_option($setting) 関数は wp-includes/functions.php で定義されていて、 以下のようにフィルタフック pre_option_* が定義されている:

function get_option( $setting, $default = false ) {
    global $wpdb;

    // Allow plugins to short-circuit options.
    $pre = apply_filters('pre_option_' . $setting, false);
    if ( false !== $pre )
        return $pre;
    ...(中略)...
}

つまり、 「pre_option_設定名」 というフックに登録されたフィルタが値を返すなら、 get_option はオプションの設定値ではなくフィルタが返した値を返すようになる。 前述の例なら、 「pre_option_blogname」 フックに登録された 「option_blogname_cto」 フィルタが 「仙石浩明CTO の日記」 という値を返すので、 get_option('blogname') も 「仙石浩明CTO の日記」 という値を返すようになり、 結果としてブログのタイトルを変更できる、というわけ。

ただし、 前述したように CTO日記は 「仙石浩明の日記」 の一カテゴリという位置付けなので、 ブログのタイトルを変更しただけだと、 ブログ 「仙石浩明CTO の日記」 の 「仙石浩明CTO の日記」 カテゴリということで、 ページのタイトル等が 「仙石浩明CTO の日記 » 仙石浩明CTO の日記」 という冗長なものになってしまう。 そこで、 以下のようなスクリプトを 「functions.php」 に追加して、 タイトルとカテゴリ名が同じときはカテゴリ名が表示されないようにする:

function single_cat_title_cto($category_name) {
    $name = get_option('blogname');
    if ($category_name == $name) return "";
    return $category_name;
}
add_filter('single_cat_title', 'single_cat_title_cto');

single_cat_title フックは、 wp-includes/general-template.php で定義されていて、 ページのタイトルなどに表示されるカテゴリ名を変更することができる。

以上で、 「仙石浩明の日記」 の一カテゴリを 「仙石浩明CTO の日記」 の体裁で見せることができるようになる。 しかし、 元が 「仙石浩明の日記」 であるだけに、 リンク先が全て 「仙石浩明の日記」 のページになってしまう。 例えば、 「仙石浩明CTO の日記」 のトップページの一番下に、 「古い投稿 »」 というリンクがあるが、 このリンク先が https://www.gcd.org/blog/page/2/ になってしまい、 たどると 「仙石浩明の日記」 のトップページの 2ページ目へ遷移してしまう。

また、 本文中 (あるいはサイドバー) に現れるリンクも、 DB のデータは 「仙石浩明の日記」 のパーマリンクを用いているので、 たとえそれが 「仙石浩明CTO の日記」 カテゴリに含まれていても、 そのリンクをたどると 「仙石浩明の日記」 の記事として表示されてしまう。

そこで、 遷移先も 「仙石浩明CTO の日記」 として表示したいリンクを、 フィルタで書き換えることにした。 つまり DB のデータは 「仙石浩明の日記」 へのリンクのままで、 ブラウザに送信する前に都度書き換える。

対象となるリンクは、 記事本文中だけでなく、 前述したページナビ 「古い投稿」 「新しい投稿」 や、 サイドバー (「人気記事」 や 「最近の投稿」) にも現れる。 ページ丸ごと (つまり HTTP レスポンス丸ごと) HTML を書き換えられるフックがあるとよかったのだが、 残念ながらそういうフックは定義されていないようだ。 以下のフックそれぞれについてリンクを書き換えればよさげ:

フィルタフック フィルタが変更できる対象, 第2引数, ...
the_content 記事本文 HTML
the_category 記事の末尾に表示されるカテゴリーリストの HTML,
$separator, $parents
get_pagenum_link ページ末尾に表示されるページナビ 「古い投稿」 「新しい投稿」 の URL
post_link 記事の URL (パーマリンク), $post, $leavename
widget_text サイドバーに表示されるテキストウィジェットの HTML, $instance
wp_list_categories サイドバーに表示されるカテゴリーのリストの HTML
category_feed_link カテゴリーの RSSフィードの URL, $feed

書き換え対象のリンクを決めるために、 まず CTO日記カテゴリに属す記事の ID を取得する:

function setup_cto_id() {
    global $wpdb;
    global $is_cto_id;
    $result = $wpdb->get_results("SELECT object_id FROM wp_term_relationships WHERE term_taxonomy_id=17", ARRAY_N);
    foreach ($result as $row) {
        $is_cto_id[$row[0]] = 1;
    }
}

あるカテゴリに属す記事ID のデータを取得する関数など、 WordPress に含まれているんじゃないかと探してみたのだが、 見つからなかったので DB に問合わせて取得するようにしてみた。 毎回 DB アクセスが発生してしまうが、 キャッシュとかはアクセス数が増えてから考える (^^;)。 「term_taxonomy_id=17」 が CTO日記のカテゴリ (決め打ち ^^;)。 CTO日記カテゴリに属す記事は、 配列 $is_cto_id[記事ID] に 1 を代入しておく。

次に URL を書き換えるスクリプト:

function replace_URL_cto($matches) {
    global $is_cto_id;
    global $ORIG_SERVER_NAME;
    if (!is_array($is_cto_id)) {
        setup_cto_id();
    }
    if (is_null($matches[2])) {
        if ($matches[1] == "category/cto/") return "http://$ORIG_SERVER_NAME/";
        return "http://$ORIG_SERVER_NAME/$matches[1]";
    } elseif ($is_cto_id[$matches[2]]) {
        return "http://$ORIG_SERVER_NAME/$matches[1]";
    }
    return $matches[0];
}

function convert_URLs_cto($text) {
    $textarr = preg_split("/(<.*>)/U", $text, -1, PREG_SPLIT_DELIM_CAPTURE);
    $stop = count($textarr);
    for ($i = 0; $i < $stop; $i++) {
        $content = $textarr[$i];
        if (strlen($content) > 0) {
            $content = preg_replace_callback(
                    '@https://www.gcd.org/blog/(\d+/\d+/(\d+)|category/cto/)@',
                    'replace_URL_cto', $content);
        }
        $output .= $content;
    }
    return $output;
}

顔文字を画像に変換して表示するフィルタ wptexturize (wp-includes/formatting.php で定義されている) を参考にさせてもらった。 preg_replace_callback() を使って書き換え対象リンクを探し、 replace_URL_cto で記事ID がCTO日記カテゴリに属す ($is_cto_id[$matches[2]] が TRUE) 場合のみ書き換える。

最後に、 この書き換えフィルタ replace_URL_cto を前述したフィルタフックに追加:

add_filter('the_content', 'convert_URLs_cto');
add_filter('the_category', 'convert_URLs_cto');
add_filter('get_pagenum_link', 'convert_URLs_cto');
add_filter('post_link', 'convert_URLs_cto');
add_filter('widget_text', 'convert_URLs_cto');
add_filter('wp_list_categories', 'convert_URLs_cto', 12000);
add_filter('category_feed_link', 'convert_URLs_cto');

wp_list_categories にフィルタを追加すると Category Order プラグインと衝突するので、 優先順位を下げて Category Order プラグインの後で実行されるようにしている。

また、 the_category, post_link, widget_text, category_feed_link 各フックは、 2つ以上の引数を持つが、 第1引数 (書き換え対象の HTML あるいは URL) のみ使用するので引数の数 (add_filter の第3引数) を省略している。

Filed under: システム構築・運用,プログラミングと開発環境 — hiroaki_sengoku @ 07:46
2010年1月28日

本ブログを livedoorブログから WordPress へ引越しました (URL は変更なし) hatena_b

このブログ 「仙石浩明の日記」 は、 今まで livedoorブログの有料プラン (月額 315円) を利用していた。 比較的自由にデザインをカスタマイズできること、 URL のドメインを自由に設定できることから、 2006年3月9日にこのブログを開設して以来、 今まで 4年近く使い続けてきた。 しかし自前サーバでない不便さ、隔靴掻痒感はいかんともしがたく、 乗り換えを検討したことは何度もあって、 そのたびに不便さと乗り換えの面倒くささを天秤にかけて踏み切れずにいた。

例えば、 HTML文法エラーを修正できない問題点を見つけたときや、 ブログのサイドバーにある 「livedoor Reader」 「RSS」 「livedoor Blog」などのバナーを非表示にしていたら、 ライブドアから警告メールが来て、 やむなくバナー表示を復活させたときなど。 なかでも、 昨年ブログ管理画面 (有料プラン) がリニューアルされたときは、 とても大きなフラストレーションを感じた。 ライブドアはよかれと思ってリニューアルしているのだろうが、 JavaScript を多用した新管理画面は、 私にとっては使いにくいことこのうえないし、 生ログ取得スクリプトも使えなくなった。

そして、 今年に入って 「ブログ共通ヘッダー」 (最上端に表示される livedoor Blogのロゴ, 所属カテゴリ, テキスト広告) が勝手に表示されるようになったのが最後の背中のひと押しとなった。 これを非表示にしたければ PRO プラン (月額 315円) ではなく ADVANCE プラン (月額 840円) に変更しろという趣旨 (要は値上げ) なのだと思うが、 何の連絡もなく画面の一番目立つ最上端に、 醜悪なヘッダー (ADVANCE プランへの移行を促すためにわざと醜悪にしている? *_*) を勝手に挿入するデリカシーの無さに驚き呆れ、 自前サーバへの引越を決めた。

common header

ブラウザ画面最上端 ↑ の 「ブログ共通ヘッダー」 (所属カテゴリ 「IT > プログラミング 」 と 「受験にまつわるエピソードを教えてください」 という広告? 表示) は、 ライブドアのサーバが HTML 中に自動挿入する以下の JavaScript (settings/header.js) によって body 要素へ追加 (appendChild) されている。

(function () {
    var hd = document.createElement('div');
    var str = '';
    str += '<div id="header2" style="z-index:10001"><table cellspacing="0" class="blog-common-header" id="header">';
        ...(中略)...
    hd.innerHTML = str;
    document.body.appendChild(hd);
        ...(後略)...

ユーザがカスタマイズ可能な範囲の外であるため、 この JavaScript の実行そのものを止めることは無理っぽいが、 追加された子要素を削除する JavaScript を実行して消すことは可能。 例えば以下のようなコードを HTML ファイルのどこかに入れておけばよい。

<script type="text/javascript" src="https://www.gcd.org/sengoku/docs/livedoor.js"></script>

livedoor.js は、 body 要素の子要素 「header2」 を取り除く (removeChild) だけの JavaScript:

(function () {
    var element = document.getElementById("header2");
    element.removeChild(element.childNodes.item(0));
})();

正月早々 「ブログ共通ヘッダー」 が勝手に表示されるようになったことに気付き、 直ちにこの対策を行なって非表示にしたのだが、 以前バナーを非表示にしたときライブドアから警告メールが来たことを考えれば、 今回も警告メールが来るのは時間の問題だろうと、 引越を急いだ次第。

私はブログを書くときも HTML で書いていて、 あまり CMS 的な機能は必要としないので、 トラックバックとコメントを受付けるだけの簡単な Web アプリを書こうと思っていたのだが、 試しにインストールしてみた WordPress が思いの外シンプルな作りで、 ちょっといじっているうちに簡単にカスタマイズできてしまったので、 これを使うことにした。

livedoorブログでエクスポートしたデータを (Movable Type / TypePad のデータとして) WordPress でインポートしたら、 (pre 要素なのに) 行頭の空白文字が削除されてしまって往生したが、 大した量でもなかったので手作業で修正してしまった。 もう少し量が多かったら真面目に PHP コードを追ったのだが...

こんなに簡単にできるなら、 もっと早く WordPress へ移行すればよかった。 既存のテーマを見よう見まねでいじっただけなのだが、 以前のデザインをほとんどそのまま踏襲することができた (むしろ余計な div 要素を排除できたので、よりシンプルになった)。

livedoorブログと WordPress では URL (パーマリンク, permalink) の形式が異なる。 例えば、 あるエントリの URL は、 livedoorブログ (旧URL) と WordPress (新URL) で次のようになる:

旧: livedoorブログ新: WordPress
ベースURL http://blog.gcd.org https://www.gcd.org/blog
エントリ /archives/51168556.html /2008/04/154/
月別表示 /archives/2008-04.html /2008/04/
カテゴリ /archives/cat_50026701.html /category/enlighten/
RSS /index.rdf /feed/rdf/

一見して、 WordPress のパーマリンクの方が分かりやすい。 正確に言えば、 WordPress のパーマリンクの形式は任意に設定できて、 livedoorブログ (Movable Type ベース) の形式に合わせることも可能だが、 エントリID (上記の例だと 51168556 と 154) が異なるので形式を合わせること自体にはあまり意味はない。

livedoorブログを利用していた時の URL がそのまま使えるように、 旧URL を新URL へ変換する以下の PHP スクリプトを書いた (switch 文で URL ごとにずらずら case を並べただけ)。

<?php
$new = NULL;
if ($_SERVER['REQUEST_URI'] == "/") {
    $new = "/blog/";
} elseif ($_SERVER['REQUEST_URI'] == "/atom.xml") {
    $new = "/blog/feed/atom/";
} elseif ($_SERVER['REQUEST_URI'] == "/index.rdf") {
    $new = "/blog/feed/rdf/";
} elseif (preg_match('/^\/archives\/(20\d\d)-(\d\d)\.html$/',
                     $_SERVER['REQUEST_URI'], $matches)) {
    $year = $matches[1];
    $mon = $matches[2];
    $new = "/blog/$year/$mon/";
} elseif (preg_match('/^\/archives\/cat_(\d+)\.html$/',
                     $_SERVER['REQUEST_URI'], $matches)) {
    $id = $matches[1];
    $new = "/blog/category/";
    switch ($id) {
    case 50026701: $new .= "enlighten/";   break;
    case 50026704: $new .= "business/";    break;
    case 50026703: $new .= "engineer/";    break;
    case 50026699: $new .= "system/";      break;
    case 50035671: $new .= "hardware/";    break;
    case 50026700: $new .= "programming/"; break;
    case 50021362: $new .= "stone/";       break;
    case 50035209: $new .= "la-fonera/";   break;
    case 50041534: $new .= "hawaii/";      break;
    case 50045637: $new .= "hongkong/";    break;
    case 50026702: $new .= "others/";      break;
    default: $new = NULL;
    }
} elseif (preg_match('/^\/archives\/(\d+)\.html$/',
                     $_SERVER['REQUEST_URI'], $matches)) {
    $id = $matches[1];
    $new = "/blog/";
    switch ($id) {
    case 50071073: $new .= "2006/03/8/"; break;
        ...(中略)...
    case 51168556: $new .= "2008/04/154/"; break;
        ...(中略)...
    case 51552583: $new .= "2009/12/184/"; break;
    case 51555097: $new .= "2010/01/185/"; break;
    default: $new = NULL;
    }
}
if ($new) {
    $host = "www.gcd.org";
    $_SERVER['SERVER_NAME'] = $host;
    $_SERVER['REQUEST_URI'] = $new;
    $_SERVER['SCRIPT_NAME'] = $new;
    $_SERVER['PHP_SELF'] = $new;
    define('WP_USE_THEMES', true);
    require('/usr/local/www/wordpress/wp-blog-header.php');
} else {
    header("HTTP/1.1 301 Moved Permanently");
    header("Location: https://www.gcd.org/blog/");
    exit();
}
?>

http://blog.gcd.org/* にアクセスすると、 ↑ この PHP スクリプトが実行される。

これに加え、 逆方向の変換、 つまり新URL から旧URL への変換も必要。 例えば 「はてなブックマーク」 へ頂いたコメント (例えば前述したエントリに対するコメント一覧) は旧URL に紐づいているので、 各エントリに旧URL を登録しておく必要がある。 WordPress には カスタムフィールド という機能があって、ひとつのエントリに複数の 「キー」 と、 各キーにその値を登録することができる。 そこで沢山の 「ブックマーク」 を頂いたエントリには、 「hatena_b」 というキーで旧URL を登録しておくことにした。 すると、以下のような PHP スクリプトでブックマーク数 を表示できる。

<?php
  $hatena_b = get_post_meta($post->ID, "hatena_b", true);
  if ($hatena_b)
    echo ' <a href="http://b.hatena.ne.jp/entry/' . $hatena_b
       . '"><img style="border:0;" src="http://b.hatena.ne.jp/entry/image/'
       . $hatena_b . '" alt="" /></a>';
?>

最後に、 ネームサーバの設定変更を行なって、 blog.gcd.org レコード (TTL = 3時間) の値を 「CNAME blog-01.livedoor.jp」 から 「CNAME www.gcd.org」 へ変更した。 1/26 19:35 に変更を行なったところ、 19:46 には最初のアクセスが www.gcd.org へ届き、 20時台には大半のアクセスが www.gcd.org へ届くようになった。 20時台にライブドア側へ届いた blog.gcd.org へのアクセスはわずかに 7件、 その後は 1 時間〜数時間ごとに、ぱらぱらアクセスがあるという状況が続いている。 旧レコードを保持しているキャッシュネームサーバが残っていて、 そのネームサーバを使ってるブラウザからのアクセスのみが、 ライブドア側へ届いているといった感じ。

More...
Filed under: システム構築・運用 — hiroaki_sengoku @ 08:26
2009年11月12日

自宅回線を 100Mbps から 200Mbps へ、帯域を倍にしてみた ~ NGN が自宅にやってきた hatena_b

私の自宅がある神奈川でも、 先月 13日から 「フレッツ 光ネクスト ファミリー・ハイスピードタイプ」 の申込受付が始まったので、 早速申し込んでみた。 今まで使っていたのは 「Bフレッツ ニューファミリータイプ」 だから、 下りの帯域が 100Mbps から倍の 200Mbps に増える。 月額利用料は変わらない。 工事費が 5827円かかるが、 キャンペーン期間中につき月額料金が 3ヶ月間無料になるので悪くない。

10/16日に NTT東日本 0120-116116 に電話して申し込んだところ、 回線切替工事まで最短で 10営業日かかり、 工事の予約状況によると最速で 11/1(日) の工事になるらしい。 続いて NTT コミュニケーションズ 0120-506-506 に電話して、 200Mbps に変更したときプロバイダ (OCN) 側の変更が必要か確認。 個人向けサービス (OCN 光 with フレッツ) は元々 200Mbps に対応しているから契約変更は必要無しとのことだったが、 あいにく私が契約しているのは法人向けサービスであるところの OCN 光アクセス IP8/IP16「Bフレッツ」プラン である。 この場合、 OCN 光アクセス IP8/IP16「フレッツ 光ネクスト」プラン への契約変更が必要となる。

NTT コミュニケーションズの担当者の話によると、 認証サーバの設定を変更するらしい。 PPPoE のユーザ名とパスワードはそのまま同じものを使っていい (新しいユーザ名も付与されるので新旧どちらのユーザ名も使える) が、 認証サーバ側の設定を変更すると旧回線 (つまり Bフレッツ) からの認証は拒否されるらしい。 すなわち認証サーバの設定変更後は、 新回線 (つまり フレッツ 光ネクスト) からでないと認証が通らない。 したがって、 回線が切り替わる前に PPPoE セッションが切れると、 旧回線のままでは再接続できなくなってしまう。 さらに、 設定変更は平日の未明 0:00 ~ 5:00 の間にしか実施しないらしい。

ということはつまり、 11/1(日) に回線工事を行なうには、 10/30(金) の未明に認証サーバの設定変更を行なうことになる。 10/30 未明から 11/1 の回線工事まで丸二日間、 もし PPPoE セッションが何らかの原因によって切れてしまうと、 工事完了後まで二度と再接続できなくなる。 最長丸二日間切れたままというのはあまりに不便なので、 11/2(月) の未明に認証サーバの設定変更を行ない、 同日午前 9:00-12:00 に回線切替工事を行なうことにした。 これなら PPPoE セッションが切れても数時間の切断で済む。

More...
Filed under: IPv6,システム構築・運用 — hiroaki_sengoku @ 10:00
2009年10月8日

__sync_bool_compare_and_swap_4 とは何か? ~ glibc をビルドする場合は、 gcc の –with-arch=i686 configure オプションを使ってはいけない

glibc-2.10.1 をビルドしようとしたら、 「__sync_bool_compare_and_swap_4 が定義されていない」 というエラーが出た:

senri:/usr/local/src/glibc-2.10.1.i386 % ../glibc-2.10.1/configure
        ...
senri:/usr/local/src/glibc-2.10.1.i386 % make
        ...
/usr/local/src/glibc-2.10.1.i386/libc_pic.os: In function `__libc_fork':
/usr/local/src/glibc-2.10.1/posix/../nptl/sysdeps/unix/sysv/linux/i386/../fork.c:79: undefined reference to `__sync_bool_compare_and_swap_4'
/usr/local/src/glibc-2.10.1.i386/libc_pic.os: In function `__nscd_drop_map_ref':
/usr/local/src/glibc-2.10.1/nscd/nscd-client.h:320: undefined reference to `__sync_fetch_and_add_4'
        ...
/usr/local/src/glibc-2.10.1.i386/libc_pic.os: In function `*__GI___libc_freeres':
/usr/local/src/glibc-2.10.1/malloc/set-freeres.c:39: undefined reference to `__sync_bool_compare_and_swap_4'
collect2: ld returned 1 exit status
make[1]: *** [/usr/local/src/glibc-2.10.1.i386/libc.so] Error 1
make[1]: Leaving directory `/usr/local/src/glibc-2.10.1'
make: *** [all] Error 2

__sync_bool_compare_and_swap_4 は gcc の組み込み関数なので、 関数が未定義であることを示す 「undefined reference to ...」 というエラーメッセージは、 誤解を招く不親切なメッセージだと思う。

__sync_bool_compare_and_swap_4(mem, oldval, newval) は、 mem が指し示すメモリの値 (4バイト分) が oldval であれば newval に変更する、 という操作をアトミックに行なう組み込み関数。 アトミック (不可分) 操作とは、 操作の途中が存在してはいけない操作のことで、 この例なら比較 (メモリの値が oldval か?) と代入 (newval に変更) が必ず 「いっぺん」 に行なわれ、 「比較だけ行なったけどまだ代入は行なわれていない」 という状態が存在しないことを意味する。

アトミックに行なうためには、 当然ながら CPU でその操作をサポートしている必要がある (複数個の命令の列で実現しようとすると、 命令列の半ばを実行中の状態が必ず存在してしまう) わけだが、 残念ながら Intel 386 プロセッサでは、 この compare_and_swap (CMPXCHG 命令) をサポートしておらず、 サポートするのは Intel 486 以降の CPU である。 テストプログラムを書いて試してみる:

#include <stdio.h>

int main() {
    int mem[1], oldval, newval;
    oldval=0;
    newval=1;
    mem[0] = 0;
    __sync_bool_compare_and_swap(mem, oldval, newval);
    printf("mem[0]=%d\n", mem[0]);
    return 0;
}

見ての通り、 mem[0] の値を oldval の値 (0) と比較し、 一致していたら newval の値 (1) を代入し、 mem[0] の値を表示するだけのプログラムである。

関数名が 「__sync_bool_compare_and_swap」 であって、 後ろに 「_4」 がついていないことに注意。 gcc が引数の型 (この例では int) を見て、 その型のビット長を後ろにつけてくれる (この例では int 型は 4 バイトなので 「_4」 をつけてくれる)。

gcc では 「-march=タイプ」 オプションを指定することによって CPU タイプを指定できる。 -march オプションを指定しなかったり (この場合は全 CPU でサポートされる組み込み関数のみ利用できる)、 あるいは -march=i386 を指定したりすると、 コンパイル時にエラーになる:

% gcc -Wall test.c
/tmp/cc4eNX6L.o: In function `main':
test.c:(.text+0x3b): undefined reference to `__sync_bool_compare_and_swap_4'
collect2: ld returned 1 exit status
% gcc -Wall -march=i386 test.c
/tmp/cc6chtFj.o: In function `main':
test.c:(.text+0x36): undefined reference to `__sync_bool_compare_and_swap_4'
collect2: ld returned 1 exit status
% gcc -Wall -march=i486 test.c
% ./a.out
mem[0]=1

いまさら i486 というのもアレなので、 今なら i686 を指定するのがよさげ。 私の手元にはいまだ PentiumIII マシンがあるものの、 PentiumIII より古いマシンはない (昨年 ML115 と SC440 を買ったとき PentiumII マシンを引退させた) ので、 pentium3 を指定すれば SSE (Streaming SIMD Extensions) が利用できるようになるが、 glibc をビルドするときに必要かというと、 たぶん必要ない。

というわけでエラーの原因は分かったが、 では glibc をビルドするときは、 どうすればいいだろうか?

とりあえず google で検索してみたら、 gcc の configure オプションに 「--with-arch=i686」 を指定して gcc をビルドする必要がある、 と書いてあるページが見つかった。

--with-arch オプションは、 -march のデフォルトを設定するための configure オプションである。 つまり 「--with-arch=i686」 を指定して gcc を再インストールすると、 gcc に -march オプションをつけなくてもデフォルトが i686 になる。 なるほど確かにそうすれば、 glibc 側で何も変更せずに __sync_bool_compare_and_swap_4 関数が使えるようになりそうである。

いまどき i686 以前の CPU 用のコードが必要になりそうなケースは滅多にないだろうから、 -march オプションのデフォルトを i686 にするのも悪い選択ではないように思えた。 gcc をビルドし直すのは面倒だなーと思いつつも、 ついでに gcc のバージョンを上げておこうと gcc-4.3.4 をダウンロードしてきて 「--with-arch=i686」 付でビルドしてみた。

ところが!

More...
Filed under: システム構築・運用,プログラミングと開発環境 — hiroaki_sengoku @ 09:39
2009年9月14日

文字化けしていなくても MySQL 内の文字コードが正しくない場合がある hatena_b

MySQL 5 からテーブルごとに文字列のエンコーディングを指定できるようになった (「そんなことは知ってるYO!」という人も多いと思うので、 そういう人は「これからが本題」 の部分まで読み飛ばして欲しい)。 例えばテーブルを作るときに、

mysql> CREATE DATABASE test;
Query OK, 1 row affected (0.05 sec)

mysql> USE test
Database changed
mysql> CREATE TABLE user ( name VARCHAR(255) ) CHARSET=utf8;
Query OK, 0 rows affected (0.05 sec)

などと 「CHARSET=utf8」 を指定すれば、 文字列を UTF-8 エンコーディングで格納する。 「CHARSET」 すなわち 「文字集合」 と、 エンコーディング (文字符号化) は本来別の概念であるが、 MySQL の場合は両者をまとめて CHARSET ないし character_set と呼んでいるので、 ここではそれを踏襲してキャラクタセットと呼ぶことにする。 MySQL のシステム変数のうちキャラクタセットに関連するものは、 以下のように沢山ある。

mysql> SHOW VARIABLES LIKE 'character\_set\_%';
+--------------------------+--------+
| Variable_name            | Value  |
+--------------------------+--------+
| character_set_client     | latin1 |
| character_set_connection | latin1 |
| character_set_database   | latin1 |
| character_set_filesystem | binary |
| character_set_results    | latin1 |
| character_set_server     | latin1 |
| character_set_system     | utf8   |
+--------------------------+--------+
7 rows in set (0.00 sec)

たくさんあってややこしいが、 重要なのは 「character_set_client」 と 「character_set_connection」 「character_set_results」 で、 この3変数はクライアントがクエリを送信し、 クエリ結果を受信するときのキャラクタセットを設定する。 charset コマンド (あるいは SET NAMES) を使うと、 クライアント側のキャラクタセットに関係するこの3変数を一度に変更できるので、 特に必要がなければこの3変数は常に同じキャラクタセット、 すなわちクライアント側で送受信するキャラクタセットに一致させておくとよい (PHP スクリプトから MySQL をアクセスするときは、 mysql_set_charset() を使ってクライアント側のキャラクタセットを設定する)。

mysql> CHARSET utf8
Charset changed
mysql> SHOW VARIABLES LIKE 'character\_set\_%';
+--------------------------+--------+
| Variable_name            | Value  |
+--------------------------+--------+
| character_set_client     | utf8   |
| character_set_connection | utf8   |
| character_set_database   | latin1 |
| character_set_filesystem | binary |
| character_set_results    | utf8   |
| character_set_server     | latin1 |
| character_set_system     | utf8   |
+--------------------------+--------+
7 rows in set (0.00 sec)

mysql> INSERT INTO user VALUES ('仙石 浩明');
Query OK, 1 row affected (0.00 sec)

mysql> SELECT name, HEX(name) FROM user;
+-----------------+--------------------------------+
| name            | HEX(name)                      |
+-----------------+--------------------------------+
| 仙石 浩明      | E4BB99E79FB3E38080E6B5A9E6988E |
+-----------------+--------------------------------+
1 row in set (0.00 sec)

このように 「select HEX()」 で確認すると、 文字列が正しく UTF-8 エンコーディングで格納されていることが確認できる。

蛇足だが、mysql クライアントが GNU readline を使っている場合は、 ~/inputrc などで 「set convert-meta Off」 を設定しておく。 デフォルトの readline では convert-meta が On なので、 キャラクタの最上位ビット (MSB) を 0 にしてしまう。 つまり UTF-8 (および EUC-JP や Shift_JIS) などの 8bit キャラクタセットだと、 MSB が 1 である文字が正しく送信されない。 例えば 「あ」 (UTF-8 で E3 81 82) を入力しようとしても、 MSB が落ちた 「c^A^B」 (63 01 02) が送られてしまう。

「character_set_server」 が latin1 になっていて気持ち悪いかも知れないが、 このシステム変数は新しく database を作るときのデフォルトを設定するものなので、 (latin1 な database は金輪際作らないというのでも無い限り) 変更する必要はない。

latin1 になっているもう片方の変数 「character_set_database」 は、 デフォルト database に合わせて (つまり USE コマンドを発行するごとに) サーバがこの変数を変更するので、 これもユーザが変更する必要はない。

前置きが長くなったが、これからが本題

UTF-8 なテーブルを読み書きする際は、 「charset utf8」 コマンドを送信してクライアント側のキャラクタセットを UTF-8 に設定すればよいのであるが、 デフォルトが latin1 であるクライアントも多い。 PHP などから MySQL サーバにアクセスする場合なども、 (PHP のビルド方法にも依存するが) デフォルトは latin1 になっている (PHP の場合 mysql_client_encoding() で確認できる)。 このようなクライアントをデフォルトの latin1 のままで使うとどうなるだろうか?

More...
Filed under: システム構築・運用 — hiroaki_sengoku @ 08:19
2009年1月8日

wpa_supplicant のバグ: TLS拡張 (TLS Extensions) 対応の OpenSSL と使うと、WPA EAP-TLS が常に失敗する

あけましておめでとうございます。 今年もよろしくお願いします。

自宅の無線LAN を WPA2-EAP (WPA2 エンタープライズ) にして 1ヶ月になるが、 すこぶる快適。 アクセスポイントがエンタープライズモードに対応していなければいけないのと、 RADIUS サーバが必要である上、 EAP-TLS を用いる場合は SSL クライアント証明書を発行する 認証局 (CA) を作る必要まであるので、 自宅LAN での導入にはややハードルが高いが、 いったん導入してしまえば WPA/WPA2 パーソナルより手間がかからない。

WPA/WPA2 パーソナルだと、 秘密のパスワードを全ての無線LAN 端末で共有するので、 端末の台数が増えてくるとどんどん漏洩のリスクが高まる。

自宅LAN でそんなに端末があるのか? という突っ込みが入りそうだが、 格安 NetBook や、 無線LAN 機能付スマートフォンなどを買っていると、 自宅LAN とはいえ、 「エンタープライズ」 的なニーズが出てくる。

WPA2 エンタープライズで認証方式として EAP-TLS を使うと、 無線LAN 端末にはそれぞれ個別の証明書をインポートしておくだけでよく、 万一その端末を紛失しても、 その端末の証明書を無効にするだけで済む。

EAP-TLS には対応機器/OS が多いというメリットもある。 無線LAN 端末として、 Windows VISTA, Windows XP, Ubuntu 8.04 LTS などを使ってみたが、 いずれもあっけないくらい簡単に接続できてしまった。 証明書さえインポートしておけば (Windows ならダブルクリックだけ)、 あとはパスワードを入力しなくてもいいぶん WPA/WPA2 パーソナルより設定が簡単。

既存の OS (Linux の場合はディストリビューション) で無線接続できるようになったので、 次は私が独自に構築した GNU/Linux (いわば my distribution) 上で WPA2 EAP-TLS 接続を試みた。

まず wpa_supplicant 0.5.11 をダウンロードしてコンパイル。 続いて設定ファイルである wpa_supplicant.conf を書く。

network={
        ssid="XXXXXXXXXX"
        key_mgmt=WPA-EAP
        eap=TLS
        identity="olevia"
        ca_cert="/usr/local/ssl/certs/GCD_Root_CA.pem"
        client_cert="/usr/local/ssl/certs/olevia.pem"
        private_key="/usr/local/ssl/private/olevia.pem"
        private_key_passwd=""
}

「ca_cert=」 「client_cert=」 「private_key=」 にそれぞれ CA の公開鍵、端末の公開鍵、端末の秘密鍵のファイル名を指定している。 で、wpa_supplicant を実行。

# wpa_supplicant -i eth1 -c /etc/wpa_supplicant.conf -d -D wext
Initializing interface 'eth1' conf '/etc/wpa_supplicant.conf' driver 'wext' ctrl_interface 'N/A' bridge 'N/A'
Configuration file '/etc/wpa_supplicant.conf' -> '/etc/wpa_supplicant.conf'
Reading configuration file '/etc/wpa_supplicant.conf'
Priority group 0
   id=0 ssid='XXXXXXXXXX'
Initializing interface (2) 'eth1'
EAPOL: SUPP_PAE entering state DISCONNECTED
EAPOL: KEY_RX entering state NO_KEY_RECEIVE
EAPOL: SUPP_BE entering state INITIALIZE
EAP: EAP entering state DISABLED
        ...(中略)...

wpa_supplicant はデバッグモード (-d オプション) にすると大量のログを出力するので動作を追いにくいが、 wpa_supplicant のプログラムは状態遷移機械 (オートマトン) として動作するよう書かれているので、 「EAP: EAP entering state」 の部分を追っていくとよい。 「DISABLED」 と出力されているのが、 その時点におけるオートマトンの状態。

状態遷移機械 (オートマトン) と言ってしまうと、 コンピュータ/プログラムは全てオートマトンなのであるが (^^;)、 プログラミングの方法として状態遷移機械 (state machine) と言うときは、 各状態に名前を付けて、 各状態ごとの動作を (switch 文や if ... else if ... 文などで) 分けて書く方法を指す。

状態には INITIALIZE, DISABLED, IDLE, RECEIVED, GET_METHOD, METHOD, SEND_RESPONSE, DISCARD, IDENTITY, NOTIFICATION, RETRANSMIT, SUCCESS, FAILURE の 13状態がある。 もちろん 「SUCCESS」 が受理状態。 SUCCESS 状態は、 アクセスポイントが接続を許可した状態を意味する。

ところが、ログを追っていくと、

EAP: EAP entering state RECEIVED
EAP: Received EAP-Success
EAP: EAP entering state FAILURE
CTRL-EVENT-EAP-FAILURE EAP authentication failed

FAILURE 状態 (非受理状態) に遷移している (*_*)。当然、接続できず。
その直前に 「Received EAP-Success」 と出ているにもかかわらず!

アクセスポイント側 (正確に言うと RADIUS サーバ) のログを調べてみると、 ちゃんと接続を許可している (EAP-Success を送っているのだから当然だが)。 一体これはどうしたことか?

私がコンパイルした wpa_supplicant に問題があるのかと思って、 この wpa_supplicant を、 既に接続できることが確認済みの Ubuntu 上にコピーして実行してみる。 「Received EAP-Success」 をログの中から探してみると、

EAP: EAP entering state RECEIVED
EAP: Received EAP-Success
EAP: EAP entering state SUCCESS
CTRL-EVENT-EAP-SUCCESS EAP authentication completed successfully

ちゃんと SUCCESS 状態に遷移しているし、接続もできた。
同一バイナリなのに、異なる環境 (ディストリビューション) だと、 どうして結果が異なるのか?

どのような可能性が考えられるだろうか? 腕に覚えがあるかたは、 この続きを見ずに (といっても、既にタイトルでネタバレしてしまっているが ^^;) 原因を推測してみてはいかがだろうか?

More...
Filed under: システム構築・運用 — hiroaki_sengoku @ 08:58
2008年10月27日

initramfs シェル環境 (initramfs shell environment) でジョブ制御する方法 (aka “can’t access tty; job control turned off” を消す方法) hatena_b

GNU/Linux OS のブート時に、init(8) を経由せずにシェル (/bin/sh) を実行すると、 このシェル上ではジョブ制御 (job control) が行なえない。 つまりこのシェル環境は制御端末 (controlling tty) に成れない。 これがどんなに不便かというと、 自動的に止まらないプログラム (例えばオプション無しで ping を実行したときなど) を止める方法が無いわけで、 いったんそういうプログラムを動かしてしまったら最後、 CTRL-ALT-DEL で reboot させる他なくなってしまう。

そもそも、なぜ init(8) を起動する前に /bin/sh を実行したいかというと、 ミニルート (initramfs) 上で 作業を行ないたいから。 initramfs の init (これは init(8) ではなくシェル・スクリプト) の中で、 BusyBox の /bin/sh (/bin/ash) を exec する (つまり PID=1) ことによって、 initramfs 上での作業を可能にする。

init(8) は、 GNU/Linux OS の全てのプロセスの親プロセスだが、 その万物の親すら生まれていない創世記以前に作業を行なえるメリットは数多い。 例えば、ルート・ファイル・システム (root file system) すらマウントしていない段階なので、 マウント後 (つまり init(8) 起動後) には実行不可能な操作 (xfs_repair などのファイルシステム修復操作とか) を行なうことができる。 しかもこのシェルはプロセスID が 1番なので、 このシェル環境上で root file system を「/」にマウントし、 続いて init(8) を exec すれば、 そのまま GNU/Linux OS を起動することができる。

root file system のメンテナンス等は、 別の起動ディスク (CD-ROM や USB メモリ等) からブートして行なうのが一般的だが、 CD-ROM ドライブや USB メモリを準備したり、 あるいは CD-ROM や USB メモリの抜き差しが必要になったりと、 なにかと面倒である。 メンテナンス用の起動パーティションを root file system とは別に用意する、 という方法も考えられるが、 メンテナンス専用のパーティションを維持管理するのが面倒くさい (普段使わないものほど陳腐化して、いざというとき役に立たない)。
initramfs だとハードディスクすら不要 (例えば PXE ブート) でメンテナンスが可能になるし、 普段 GNU/Linux OS 起動用として使ってる initramfs が、 そのまま非常用のメンテナンス環境になるため、 陳腐化する心配がない。
実は「突然死したハードディスクを復旧させる『お手軽パック』」は、 initramfs そのものだったりする。 しかも「復旧」用として作った initramfs というわけではなく、 私が普段 GNU/Linux OS を ブートするときに使っている initramfs と 全く同じものである (だからこそ ハードディスクの突然死問題 が勃発した直後にリリースできた)。

というわけで、 いいことづくめの initramfs シェル環境 (initramfs shell environment) なのだが、 「復旧お手軽パック」実行例 にもあるように、 init(8) 以前の段階で /bin/sh を実行すると

/bin/sh: can't access tty; job control turned off
#

と表示してジョブ制御がオフになってしまう。 つまりこのシェル環境では、 プログラムを実行中に control-C (^C) を押しても止める (正確にいうと SIGINT シグナルを送る) ことができない。

ジョブ制御 (^C などでシグナルを送ること) ができない状態に陥って 初めて沸き起こる制御端末 (controlling tty) に対する感謝の念なのであるが、 initramfs が役目を終えて init(8) が起動して (GNU/Linux OS がブートして) しまうと、 喉元過ぎればなんとやらで 「can't access tty」をなんとかしようという意欲は雲散霧消し、 そのままになっていた。

制御端末になれない initramfs シェル環境に対して何十回目かの悪態をついた後、 ようやく対策を立てるべく原因を調べてみることにした。

Linuxカーネル(ドライバ)のソースを読んでみたところ、 以下の端末デバイスは制御端末に成れないのです。 興味がある人は、ソース、 drivers/char/tty_io.cのtty_open()を見てみてください。

* /dev/console -- カーネルの起動時の端末。
* /dev/tty0 -- tty1~の「Linux Virtual Terminal」のうち、現在表示している物を示す。
* /dev/tty -- 現在使っている端末を示す。
* PTYのマスター側
Linux:制御端末 から引用

initramfs シェル環境で使っている端末は /dev/console だから制御端末になれない。 だから BusyBox には /dev/console という仮想的な端末ではなく、 本物のデバイスを探すための cttyhack というプログラムが付属している。 /bin/sh を実行する代わりに cttyhack /bin/sh を実行すれば ジョブ制御ができると BusyBox のマニュアルには書いてある。

...という解説は上に引用したページをはじめ、 WWW 上のあちらこちらのページで見かけるし、 私としても当然そんなことは先刻承知で、

/bin/sh: can't access tty; job control turned off
# tty
/dev/console
# cttyhack sh
sh: can't access tty; job control turned off
# tty
/dev/tty1
#

などと、確かに cttyhack の働きにより /dev/console ではなく /dev/tty1 を使うようになったものの、 相変わらず「can't access tty」エラーが出ているので困っているわけである。 cttyhack を使っているのにジョブ制御できないわけで、 cttyhack-- と思っていた。

前置きが長くなったが、ここからが本題である。

More...
Filed under: システム構築・運用 — hiroaki_sengoku @ 07:38
2008年3月19日

フレッツ・ドットネットを解約したら、フレッツ網 router へ ping6 できなくなった!

昨年12月26日に、 NTT東日本から 「BフレッツにおけるIPv6映像視聴等機能の標準装備について」 というお知らせが来た。 3月3日より、 「Bフレッツ」に IPv6 映像視聴等機能を標準装備するので、 フレッツ・ドット・ネットの契約が不要になるとのこと。

現在、IPv6映像視聴等機能は「フレッツ・ドットネット」にて 提供しておりますが、 平成20年3月3日(月)以降は、 ブロードバンド映像サービスのみ をご利用の場合は、 「フレッツ・ドットネット」のご契約が不要になります。 これにより解約を希望されるお客さまにつきましては、 平成20年3月3日(月) より※3受付を開始いたします。  なお、ブロードバンド映像サービス以外で、 「FdNネーム」「FdNディスク」「FdNディスクビューセレクト」「FdNナンバー」等 「フレッツ・ドットネット」サービス※4をご利用の際には、 引き続き「フレッツ・ドットネット」のご契約が必要となりますのでご注意ください。

私は IPv6 機能のためだけに「フレッツ・ドットネット」を契約していて、 「FdNネーム」「FdNディスク」「FdNディスクビューセレクト」「FdNナンバー」等の サービスを利用したことはない (「ブロードバンド映像サービス」も利用していない) ので、 フレッツ・スクウェアトップから、 サービス申込受付を選んで、「フレッツ・ドットネット」を解約した。

ところが!

フレッツ網側の v6 ルータが ping に反応しなくなった。

senri % ping6 -n router.flets.gcd.org
PING router.flets.gcd.org(2001:c90:XXXX:XXXX:2d0:2bff:fe30:b91a) 56 data bytes
From 2001:c90:XXXX:XXXX:2d0:2bff:fe30:b91a icmp_seq=1 Destination unreachable: Administratively prohibited
From 2001:c90:XXXX:XXXX:2d0:2bff:fe30:b91a icmp_seq=2 Destination unreachable: Administratively prohibited

--- router.flets.gcd.org ping statistics ---
2 packets transmitted, 0 received, +2 errors, 100% packet loss, time 1000ms

ちなみに、 この v6 ルータからの router advertisement は正常に流れてきている:

senri # tcpdump -i eth1 -vvv ip6
tcpdump: listening on eth1, link-type EN10MB (Ethernet), capture size 96 bytes
        ...
13:02:02.904899 fe80::2d0:2bff:fe30:b91a > ff02::1: icmp6: router advertisement(chlim=64, pref=medium, router_ltime=1800, reachable_time=0, retrans_time=0)(src lladdr: 00:d0:2b:30:b9:1a)(mtu: mtu=1500)[ndp opt] [class 0xe0] (len 64, hlim 255)

v6 ルータ越えの通信が全て禁止されてしまったらしく、 フレッツ網に接続している他サイトとの IPv6 通信も同様に禁止されて (Administratively prohibited) しまった。 例外は、フレッツ・スクウェアv6 へのアクセス:

senri % ping6 -n flets-v6.jp
PING flets-v6.jp(2001:c90:ff:1::1) 56 data bytes
64 bytes from 2001:c90:ff:1::1: icmp_seq=1 ttl=52 time=5.05 ms
64 bytes from 2001:c90:ff:1::1: icmp_seq=2 ttl=52 time=4.55 ms

--- flets-v6.jp ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1001ms
rtt min/avg/max/mdev = 4.554/4.803/5.053/0.258 ms

おそらく「ブロードバンド映像サービス」を提供するサーバへの IPv6 通信も 許可されているのだろう。 つまり、 「BフレッツにIPv6映像視聴等機能を標準装備」 という NTT東日本の発表のココロは、 IPv6 映像サービスへの IPv6 通信*のみ*許可するということであって、 それ以外の IPv6 通信は対象外ということのようだ。

「フレッツ・ドットネット」サービスの概要には、 「フレッツ・ドットネットサービス機能一覧」として、

・FdNネームが1つ利用できます。
・FdNディスク(100MB)で、ファイル共有が利用できます。
・FdNディスク(100MB)には、最大10のグループメンバーを登録できます。
※NTT東日本が無料で提供する専用ソフトウェア「FLET'S.Netメッセンジャーにより、 ファイル転送やビデオチャットが利用できます。

が列挙されているのみであって、 IPv6 通信の許可/不許可について言及していないのは、 とてもミスリーディングな記述だと思う。

慌てて再度フレッツ・ドットネット契約を (フレッツ・スクウェアで) 申込むと、 10分ほどで再び IPv6 通信ができるようになった:

senri % ping6 -n router.flets.gcd.org
PING router.flets.gcd.org(2001:c90:XXXX:XXXX:2d0:2bff:fe30:b91a) 56 data bytes
64 bytes from 2001:c90:XXXX:XXXX:2d0:2bff:fe30:b91a: icmp_seq=1 ttl=64 time=3.11 ms
64 bytes from 2001:c90:XXXX:XXXX:2d0:2bff:fe30:b91a: icmp_seq=2 ttl=64 time=0.920 ms

--- router.flets.gcd.org ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1001ms
rtt min/avg/max/mdev = 0.920/2.019/3.118/1.099 ms

フレッツ網を経由した IPv6 通信を利用しているかたは、 たとえフレッツ・ドットネットサービスを利用していなくても、 フレッツ・ドットネットを解約すべきではないので、 ご注意のほどを!

Filed under: IPv6,システム構築・運用 — hiroaki_sengoku @ 15:32
2008年1月31日

リモートの p0f (passive fingerprinting) の結果を参照してスパム対策を行なう hatena_b

p0f は、 通信相手の OS を受動的に特定するツールで、 迷惑メール送信などの スパム行為を行なう「敵」を知る手段として有用である。 例えば、もし (あくまで仮定の話だが) 受信するメールのほとんどすべてが Linux や FreeBSD などの UNIX 系サーバから送信されるメールであって、 Windows マシンから送られてきたメールのほとんどすべてが迷惑メールであったなら、 Windows マシンからのメールを排除するという対策は合理的なものとなるだろう。

もちろん、Windows を使ってマトモなメールを送ってくるケースもあるだろうから、 Windows から送られたメールを全て排除するのは現実的ではないが、 p0f での判定結果と、 その他の手段 (例えば送信元 IP アドレス) での判定結果を組合わせて 迷惑メールであるか否かの判断を行なえば、 より精度の高い迷惑メール排除が可能になる。

ところが、 p0f は通信相手から送られてくる IP パケットを元に、 通信相手の OS を特定するツールであるから、 間にファイアウォールや NAT (IPアドレス・ポート変換) を行なう機器があると、 通信相手ではなくファイアウォールや NAT について調べてしまう。 だから、メールを受信するサーバがファイアウォールの内側にある場合は、 意味ある結果が得られないし、 外側にある場合だとメールを受信するサーバとは別の場所 (つまりファイアウォールの内側) で スパム判定を行ないたくなるものだろう。 例えばメールサーバは DMZ 上にあるが、 迷惑メール判定は LAN 内のマシンで行ないたい場合など。

私の個人サイト GCD は、 b フレッツに PPPoE 接続している。 p0f は調べる通信のインタフェース名を -i オプションで指定する必要があるが、 (1) PPPoE だからインタフェース名 (ppp0~) が変わることがある。 また、 PPPoE を行なうゲートウェイマシンは二台ある (冗長構成) ので、 (2) アクティブ側で p0f を実行しないと意味がない。 さらに、 メールサーバは (メールボックスを一ヶ所にまとめたかったので) 一台だけであり、 (3) 異なるサーバ上 (アクティブ側のゲートウェイ) で動いている p0f の結果を メールサーバから参照しなければならない。

以上 (1) ~ (3) の 3点を満たすための構成を考えてみた。

まず (1) と (2) は、pppd の ip-up スクリプトから p0f を実行すればよい。 例えば、ip-up で

command=$0
interface=$1
        ...
case $command in
    *ip-up)
        p0f -i $interface -Q /var/run/p0f-sock \
            'port 25 and (not src net 192.168.0.0/16)' \
            -u stone -d -t -o /var/log/p0f.log
        ;;
    *ip-down)
        killall p0f
        ;;
esac

などと p0f を起動し、ip-down で p0f を終了させる。 これでアクティブ側のゲートウェイ上でのみ p0f が動く。

p0f による判定結果は、 p0f の -Q オプションで指定した UNIX ドメイン・ソケット (上記の例では、 /var/run/p0f-sock) を介して 問合わせることができるが、 UNIX ドメイン・ソケットなので当然のことながら 別のマシンからは問合わせることができない。 そこで stone に転送させる:

stone /var/run/p0f-sock 12345 &

アクティブ側のゲートウェイは、 仮想ルータの IP アドレス 192.168.1.1 を持っているので、 「192.168.1.1:12345」へアクセスすれば、 それを stone が /var/run/p0f-sock へ中継してくれるので、 (3) p0f の結果を参照できる。

p0f の結果を参照するサンプルプログラムとして、 p0f には perl で書かれた p0fq.pl と、 C で書かれた p0fq.c が付属しているが、 あいにくどちらも UNIX ドメイン・ソケットにしか対応していない (当たり前)。 ちょっといじってリモート上の p0f へ (stone 経由で) アクセスできるようにしてみる。

p0fq.pl へのパッチ:

--- test/p0fq.pl.org        2006-08-21 23:11:10.000000000 +0900
+++ test/p0fq.pl        2008-01-31 08:00:14.652880068 +0900
@@ -30,8 +30,14 @@
                  $src->intip(), $dst->intip(), $ARGV[2], $ARGV[4]);
 
 # Open the connection to p0f
-my $sock = new IO::Socket::UNIX (Peer => $ARGV[0],
+my $sock;
+if ($ARGV[0] =~ /^[\-\w]+:\d+$/) {
+    $sock = new IO::Socket::INET (PeerAddr => $ARGV[0],
                                  Type => SOCK_STREAM);
+} else {
+    $sock = new IO::Socket::UNIX (Peer => $ARGV[0],
+                                  Type => SOCK_STREAM);
+}
 die "Could not create socket: $!\n" unless $sock;
 
 # Ask p0f

「IO::Socket::UNIX」を「IO::Socket::INET」に変更するだけで済む。

p0fq.c へのパッチ:

--- test/p0fq.c.org        2006-08-21 21:29:49.000000000 +0900
+++ test/p0fq.c        2008-01-31 08:05:55.499326450 +0900
@@ -16,6 +16,7 @@
 
 #include <sys/types.h>
 #include <sys/socket.h>
+#include <netdb.h>
 #include <stdio.h>
 #include <stdlib.h>
 #include <unistd.h>
@@ -40,6 +41,7 @@
   struct p0f_response r;
   _u32 s,d,sp,dp;
   _s32 sock;
+  char *str;
   
   if (argc != 6) {
     debug("Usage: %s p0f_socket src_ip src_port dst_ip dst_port\n",
@@ -55,12 +57,37 @@
   if (!sp || !dp || s == INADDR_NONE || d == INADDR_NONE)
     fatal("Bad IP/port values.\n");
 
+  if ((str=strchr(argv[1], ':'))) {
+    struct addrinfo *ai = NULL;
+    struct addrinfo hint;
+    int err;
+    *str++ = '\0';
+    hint.ai_flags = 0;
+    hint.ai_family = AF_INET;
+    hint.ai_socktype = SOCK_STREAM;
+    hint.ai_protocol = IPPROTO_TCP;
+    hint.ai_addrlen = 0;
+    hint.ai_addr = NULL;
+    hint.ai_canonname = NULL;
+    hint.ai_next = NULL;
+    err = getaddrinfo(argv[1], str, &hint, &ai);
+    if (err) {
+      if (err == EAI_SYSTEM) pfatal("getaddrinfo");
+      else fatal("getaddrinfo(%s,%s): %s\n",
+                 argv[1], str, gai_strerror(err));
+    }
+    memcpy(&x, ai->ai_addr, ai->ai_addrlen);
+    sock = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol);
+    freeaddrinfo(ai);
+    if (sock < 0) pfatal("socket");
+  } else {
   sock = socket(PF_UNIX,SOCK_STREAM,0);
   if (sock < 0) pfatal("socket");
 
   memset(&x,0,sizeof(x));
   x.sun_family=AF_UNIX;
   strncpy(x.sun_path,argv[1],63);
+  }
 
   if (connect(sock,(struct sockaddr*)&x,sizeof(x)))  pfatal(argv[1]);
 

getaddrinfo を呼び出すための準備に行数を費やしているので 複雑に見えるかも知れないが、 本質は プロトコル・ファミリ (protocol family) を AF_UNIX から AF_INET に変更しただけである。

(p0f を実行しているマシンとは異なるマシン上で) p0fq を実行してみる:

% p0fq 192.168.1.1:12345 81.36.137.136 2943 60.32.85.220 25
Genre    : Windows
Details  : 2000 SP4, XP SP1+
Distance : 21 hops
Link     : pppoe (DSL)

上記は、 メール送信元 (81.36.137.136 のポート 2943番) が GCD の MX (60.32.85.220 のポート 25番) へメールを送ってきた通信の p0f による判定結果。 メールサーバで、 メールヘッダにメール送信元のポート番号も出力するようにしておけば、 メールを受信するユーザが自前のメール振り分けプログラム (procmail など) を使って p0f の判定結果を参照できる点がミソ。

81.36.137.136 はスペインのプロバイダの IP アドレスらしいが、 逆引きしてみると 136.Red-81-36-137.dynamicIP.rima-tde.net となるので 動的に割当てられているアドレスなのだろう。 これは p0f の結果に pppoe (DSL) と出ていることと符合する。 そして Windows 2000 SP4 か Windows XP SP1 以降を使って 送信していることが分かる。

Filed under: システム構築・運用 — hiroaki_sengoku @ 08:55
2007年10月12日

ハードウェア・ウォッチドッグ・タイマー iTCO_wdt のススメ hatena_b

極めて稀とはいえ、Linux もハングすることはある。 ハードウェア自体には何ら異常はなく、 リセットスイッチを押したら正常に再起動してしまって、 何が問題だったか分からずじまい、という経験は誰にでもあるのではなかろうか。 原因不明のハングが全く無くなるのが理想ではあるのだが、 ハングして止ったままになるよりは、 自動的にリセットがかかって再起動してくれたほうがいい、という場合もあるだろう。

もちろんハードウェア障害が原因でハングしてしまった場合は、 リセットスイッチを押しても解決にはならない。 再起動を試みることにより、障害がより致命的になる可能性もあるので、 ウォッチドッグ・タイマーを設定する際は、 「止ったまま」と「自動再起動」とどちらがマシか天秤にかける必要がある。

そんなとき、 ウォッチドッグ・タイマー (watchdog timer) が便利。 一定時間 (例えば 30秒) 放置すると、 システムを自動的にリセットするタイマーである。 この自動リセットを回避するには、 定期的に (30秒以内に) タイマーを元に戻す (以下、番犬 (watchdog) に「蹴りを入れる」と略記) 必要があるわけで、 システムが正常に動作している時は 定期的に「蹴り」を入れ続けるようなプログラムを走らせておく。 で、 カーネルがハングしたなどの理由によって 「蹴り」を入れるプログラムが動かなくなると、 自動的にシステム・リセットがかかって ハング状態を脱出できる、という仕掛け。

ソフトウェアにどんなトラブルが起きても確実に再起動を行なわせるには、 ハードウェアで物理的にリセット スイッチを押す ハードウェアを用いるのが一番であるが、 まずはお手軽にソフトウェア版を利用してみることにした。
仙石浩明の日記: ウォッチドッグ タイマ から引用

わざわざハードウェア・ウォッチドッグ・タイマーを買ってきて 組み込むのは大変と思ったので、 上に引用した日記 (2006年5月) で書いたように ソフトウェア版ウォッチドッグ (softdog.ko モジュール) を使っていたのだが、 実はインテル・チップセットであれば大抵の PC に標準で ハードウェア・ウォッチドッグ・タイマーがついていた (何たる不覚 orz)。

Intel TCO Timer/Watchdog
Hardware driver for the intel TCO timer based watchdog devices. These drivers are included in the Intel 82801 I/O Controller Hub family (from ICH0 up to ICH8) and in the Intel 6300ESB controller hub.
linux/drivers/char/watchdog/Kconfig から引用

つまり Intel の ICH には最初から ハードウェア・ウォッチドッグ・タイマーがついていたようである。 最近の Linux カーネルには、 このウォッチドッグ・タイマーのドライバが含まれているので早速使ってみた。 というか、 Linux 2.6.22.9 を使っていたら、 このドライバ・モジュール iTCO_wdt が自動的に読み込まれていた (^^;) ので、 このウォッチドッグ・タイマーの存在に気づいた、という次第。 /dev/watchdog に何か書込んでみるだけで (例えば「echo @ > /dev/watchdog」を実行)、 タイマーがスタートした (/dev/watchdog が存在しない場合は、 「mknod /dev/watchdog c 10 130」を実行する)。

そして 30秒後、勝手にリセットがかかった (めでたしめでたし)。

ウォッチドッグ タイマというと、 普通は 60秒くらいに設定しておくものだとは思うが、 自宅サーバの場合、一時間くらいハング状態が続いてもそんなに困らない ;) のと、 あまりタイマの間隔が短すぎると、 不用意に再起動してしまう恐れもあるので、 3600秒 (一時間) に設定している。 つまり一時間以内にタイマをリセットしないと、 自動再起動が行なわれる。
仙石浩明の日記: ウォッチドッグ タイマ から引用

じゃ、iTCO_wdt.ko モジュールでも同様に heartbeat=3600 を指定すればいいのかな と思っていたら、 heartbeat は最大 613 秒までしか設定できない (TCO v2 の場合) ようである。 わずか 10分足らずでは不用意に再起動してしまう恐れ大。 そこで、 監視プログラムが直接 /dev/watchdog に「蹴り」を入れる代わりに、 監視プログラムは /var/run/watchdog に「蹴り」を入れることにして、 /var/run/watchdog を監視して /dev/watchdog に「蹴り」を入れる 「蹴り代行」デーモンを走らせておくことにした。

つまり、監視プログラムは 20分に一度 /var/run/watchdog に「蹴り」を入れるだけで、 あとは「蹴り代行」デーモンが 5秒に一度、 /dev/watchdog に「蹴り」を入れ続けてくれる。 だからウォッチドッグ・タイマーのドライバの設定は、 デフォルト (30秒) のままで済むし、 また「蹴り代行」デーモンの設定次第で、 20分といわずもっと長い余裕を持たせることも可能。

/service/watchdog/run

#!/usr/bin/perl
use strict;
use warnings;
$| = 1;
my $watchdog_uid = getpwnam("adsl_check");
my $watchdog_gid = getgrnam("watchdog");
my $watchdog_file = "/var/run/watchdog";
my $watchdog_dev = "/dev/watchdog";
print "start\n";
if (! -f $watchdog_file) {
    if (!open(WATCHDOG, ">$watchdog_file")) {
        print "can't create $watchdog_file exiting...\n";
        exit 1;
    }
    close(WATCHDOG);
    chown $watchdog_uid, $watchdog_gid, $watchdog_file;
}
($(, $)) = ($watchdog_gid, $watchdog_gid);
($<, $>) = ($watchdog_uid, $watchdog_uid);
while (-z $watchdog_file) {
    sleep 5;
}
print "confirmed $watchdog_file\n";
truncate($watchdog_file, 0);
if (!open(WATCHDOG, ">$watchdog_dev")) {
    print "can't open $watchdog_dev exiting...\n";
    exit 1;
}
select(WATCHDOG);
$| = 1;
select(STDOUT);
for (my $i=0; $i < 240; $i++) {
    print WATCHDOG "\@\n";
    sleep 5;
}
print WATCHDOG "\@\n";
close(WATCHDOG);
print "exiting...\n";
exit 0;

「/service/watchdog/run」というパス名からも分かる通り、 このスクリプトは daemontools 配下で動かしている。 このスクリプトは、 20分間 (5 秒 * 240) /dev/watchdog に蹴りを入れ続けると終了する。 そして daemontools がこのスクリプトを再起動すると、 /var/run/watchdog の存在を確認した上で再び蹴りを入れ続ける。 つまり、 20 分間以上 /var/run/watchdog に蹴りが入れられないと、 この「蹴り代行」スクリプトは止ってしまい、 /dev/watchdog への蹴りも止ってしまう。

ここでなぜ 20分毎にこのスクリプトを終了するようにしているかというと、 daemontools の動きも監視対象に含めたいから。 つまり、システムの負荷が高くなり過ぎて daemontools による再起動に時間がかかるような事態になっても、 /dev/watchdog への蹴りが止る。

まとめると、 /var/run/watchdog への蹴りが止ったり、 あるいは daemontools による再起動が滞ったりすると、 /dev/watchdog への蹴りも止ってしまって、 ウォッチドッグ・タイマーが時間切れになり、 ハードウェア的に自動リセットがかかる、という仕掛け。

私は他のマシンから

ssh server "echo '@' > /var/run/watchdog"

などと ssh でアクセスするよう cron に設定している。 ssh が成功すれば /var/run/watchdog へ書き込み、 すなわち蹴りが入れられるので、 蹴り代行スクリプトによってウォッチドッグ・タイマーに蹴りが入れられる。

Filed under: システム構築・運用,ハードウェアの認識と制御 — hiroaki_sengoku @ 07:19
2007年9月18日

NFS と AUFS (Another Unionfs) を使って、ディスクレス (diskless) サーバ群からなる低コスト・高可用な大規模負荷分散システムを構築する hatena_b

ディスクレス (diskless) サーバを多数運用しようとしたときネックとなるのが、 NAS (Network Attached Storage) サーバの性能。 多数のディスクレスサーバを賄え、かつ高信頼な NAS サーバとなると、 どうしても高価なものになりがちであり、 NAS サーバ本体の価格もさることながら、 ディスクが壊れたときの交換体制などの保守運用費用も高くつく。

それでも、多数のハードディスク内蔵サーバ (つまり一般的なサーバ) を 運用して各サーバのディスクを日々交換し続ける (運用台数が多くなると、 毎週のようにどこかのディスクが壊れると言っても過言ではない) よりは、 ディスクを一ヶ所の NAS にまとめたほうがまだ安い、 というわけで NAS/SAN へのシフトは今後も進むだろう。 そもそも CPU やメモリなどとハードディスクとでは、 故障率のケタが違うのだから、 両者の台数を同じように増やせば破綻するのは当たり前。 要は、滅多に故障しないものは増やしてもいいが、 普通に故障するものは増やしてはいけない。 サーバの部品で故障する確率が桁違いに高いのはハードディスクだから、 大規模負荷分散環境においてディスクレス化は論理的必然だろう。

ハードディスクの故障率が高いのは可動部品が多いから、 というわけでハードディスクをフラッシュメモリで 置き換えようとする傾向もあるようだ。 確かに高価な NAS サーバを導入するよりは、 各サーバにフラッシュメモリを搭載する方が安上がりである可能性もある (比較的低容量であれば)。 しかしながら、 以下に述べるように NAS サーバを普通の PC サーバで実現できてしまえば、 ディスクレス化のほうが安いのは当たり前である。 サーバの台数が多くなればなるほど、 各サーバにディスク/フラッシュメモリを必要としない ディスクレス方式の方が有利になる。

とはいえ、 PC サーバの価格に慣れてしまうと、 超高価な専用サーバの世界にはもう戻れない。 そこで、 どうすればサーバ群のディスクレス化を、 低コストで行なうことができるか考えてみる。 そもそもなぜ NAS サーバが高価かと言えば、 高パフォーマンス性と高信頼性を兼ね備えようとするから。 多数のディスクレスサーバにストレージサービスを提供するのだから 高パフォーマンス性は譲れない。 となると犠牲にしてよいのは高信頼性ということになる。

信頼できなくてもよい、 つまり時々ディスクが壊れて、 書込んだはずのデータが失われても良いなら、 そこそこ高性能な PC サーバで用が足りてしまうだろう。 壊れた場合に備えて冗長化しておけば、 高信頼ではないものの、無停止性は達成できる。 もちろんマスターサーバが壊れてスレーブサーバに切り替われば、 マスターサーバにしか書込めなかったデータは失われる。

そんな NAS サーバは使えない、と言われてしまいそうであるが (^^;)、 ディスクに書込むデータで消えては困るデータとは何だろうか? 例えば Web サーバなどでは、 永続化が必要なデータは DB サーバへ書込むのが普通で、 それ以外のデータは消えては困るとは必ずしも言えないのではないか? というか消えては困るデータは DB サーバへ書けばいいのである。

さらに考えを一歩進めて、 ディスクに書込むデータは消えてもよい、 と割りきってしまうことができれば、 NAS サーバにデータを書く必要性すらなくなってしまう。 つまり NAS サーバのディスクを読み込み専用 (Read Only 以下 RO と略記) でマウントし、 書き込みはローカルな読み書き可能な (Read Write 以下 RW と略記) RAM ディスクに対して行なう。 NAS サーバは RO だから 内容が同じ NAS サーバを複数台用意して負荷分散させれば、 高パフォーマンスと故障時のフェールオーバを同時に達成できてしまう。 NAS サーバのクラスタリングが難しいのはデータを書込もうとするからであって、 書込む必要がなければ話は一気に単純になる。

もちろん全く何も書込めない NAS サーバというのはナンセンスだろう。 ここで「書く必要がない」と言っているのは、 アプリケーション実行中にアプリケーションの動作に同期して (つまり動作結果を) 「書く」必要性である。 アプリケーションとは非同期な書込み、 例えば何らかのコンテンツを配信する Web サーバを考えたとき、 あらかじめ大量の「コンテンツ」を NAS サーバへ事前に保存しておく場合や、 あるいは「コンテンツ」を定期的に更新する場合は、 (アプリケーションの動作結果とは無関係な書き込みなので) Web アプリケーションが NAS へ書込む必要はない。
むしろ、 大量の「コンテンツ」を NAS サーバに集中することは、 コンテンツ更新が素早く行なえるというメリットとなる。 多数の Web サーバそれぞれにハードディスクを内蔵して 同じコンテンツをコピーしていては、 コンテンツの更新頻度が上がってくると 全 Web サーバの内容を同期させるのが難しくなってくるからだ。

ここで重要なのは、 上記「RO NAS サーバ + RW ローカル RAM ディスク」が、 ディスクレスサーバ上で動くソフトウェア (例えば Web アプリケーションやミドルウェア) から見ると、 普通の RW ローカルディスク (つまり普通に書込み可能なハードディスク) に見えなければならないという点である。 もしソフトウェア側で特別な対応が必要だと、 ソフトウェアの改修コストがかかってしまう。 ハードウェアのコストを下げようとして ソフトウェアのコストが上がってしまっては本末転倒である。 ディスク上のデータが RO な NAS サーバから読み込まれたものであり、 ディスクへ書込んだデータが、実は RAM ディスクに書込んだだけで、 再起動によって消えてしまうものであったとしても、 ソフトウェアから見れば、 普通の RW ハードディスクディスクのように 振る舞わなければならないのである。

このように、 複数のディスク (RO NAS と RW RAM ディスク) を重ねて 一つのディスクとして見せる仕掛けを、 重ね合わせ可能な統合ファイルシステム (Stackable Unification File System) と呼ぶ。 Linux 2.6.20 以降の場合、 二種類の統合ファイルシステムが利用可能である。 後発の Aufs (Another Unionfs) を利用して、 ディスクレスサーバを作ってみた。
(あいかわらず) 前フリが長いが (^^;)、ここからが本題である。

More...
Filed under: システム構築・運用,プログラミングと開発環境 — hiroaki_sengoku @ 06:53
« Newer PostsOlder Posts »