Microsoft Word で LL 言語から差し込み文書作成
タイトルは釣り。
簡単な帳票 (Excel 系じゃなくて Word 系の文書) をプログラムから作成する必要がでてきたので調べた。テンプレートとなる文書にちょっとした変数の差し込みを行う必要があるケース。
ただし
- 旧来の Microsoft Office Document (OLE2) はサポートしない
- COM や UNO は使わない
- (ODF や OOXML の) 仕様書は読まない
LibreOffice (OpenOffice.org) や最近の Microsoft Office だと,XML ファイルを ZIP で固めているだけなので単純な差し込みだけならわりと簡単にできそう。
注意: ODF にしても OOXML にしても XML namespace を使っているので,厳密なことをいうと以下の例のようにタグ名を決め打ちで置換していてはうまくいかないことがありうる。とはいえそうそう変更しないだろうからこのままでうまくいくだろうし,気になるなら対象タグの xmlns
の部分だけバリデーション (というかサポートするターゲットかどうか判別) すればいいんじゃないかと思う。
OpenDocument Format (ODF) 編
LibreOffice 3.3.2 Writer on Ubuntu 11.04 で作成したドキュメントで検証をおこなった。
内容は「こんにちは ○○ さん」というだけの単純なもの。○○の部分にプログラムから差しこみたい。
テンプレート文書の作成
差しこみフィールドの作成のやり方。
展開
$ unzip ../hello.odt Archive: ../hello.odt extracting: mimetype inflating: content.xml inflating: manifest.rdf inflating: styles.xml extracting: meta.xml extracting: Thumbnails/thumbnail.png inflating: Configurations2/accelerator/current.xml creating: Configurations2/progressbar/ creating: Configurations2/floater/ creating: Configurations2/popupmenu/ creating: Configurations2/toolpanel/ creating: Configurations2/menubar/ creating: Configurations2/toolbar/ creating: Configurations2/images/Bitmaps/ creating: Configurations2/statusbar/ inflating: settings.xml inflating: META-INF/manifest.xml
無駄に空フォルダが多いが,実態は追える範囲。
解析と置換
文書の内容はおもに content.xml
に記述されている。
今回の主眼となる部分の抜粋 (改行やインデントは編集してある; 元は全部1行*1 )。
<text:p text:style-name="Standard"> こんにちは <text:placeholder text:placeholder-type="text"><hogehoge></text:placeholder> さん! </text:p>
単純明快。
Perl のワンライナーでざっくり置換してみた。XML を展開・再構築するほうが行儀いいんだろうけど,バリデーションを行うのでもなければ PCRE で必要十分な気がする。
$ perl -i -pe 's{<text:placeholder .*?> \s* <hogehoge> \s* </text:placeholder> }{dayflower}gxms' content.xml
これで一応完成。だけど,文書の統計情報 (何文字含まれているかとか何ページあるかとか) も文書に含まれている。基本的に文書ファイルのプロパティで表示するためだけのものなので間違っていても構わないんだけど,なんか気持ち悪いので削除しておく。
統計情報の実態は meta.xml
というファイルの <meta:document-statistic/>
タグに存在する。以下は例。
<meta:document-statistic meta:table-count="0" meta:image-count="0" meta:object-count="0" meta:page-count="1" meta:paragraph-count="1" meta:word-count="3" meta:character-count="20"/>
なので meta.xml
ファイルから該当する XML タグを削除しておけばよい。
再構築
普通に再圧縮するだけ。
$ zip -r9 ../new.odt * updating: Configurations2/ (stored 0%) updating: META-INF/ (stored 0%) ......
作成された文書を再度 LibreOffice で開いてみたところ,きちんと埋め込みされていた。
Office Open XML (OOXML) 編
Microsoft Office 2010 Word で作成したドキュメントで検証をおこなった。
テンプレート文書の作成
差しこみフィールドの作成のやり方。
展開
$ unzip ../hello.docx Archive: ../hello.docx inflating: [Content_Types].xml inflating: _rels/.rels inflating: word/_rels/document.xml.rels inflating: word/document.xml inflating: word/theme/theme1.xml inflating: word/settings.xml inflating: word/webSettings.xml inflating: word/stylesWithEffects.xml inflating: docProps/core.xml inflating: word/styles.xml inflating: word/fontTable.xml inflating: docProps/app.xml
無駄な空フォルダは少ない。結構多くのファイルに分散しているのは ODF と同等。
解析と置換
文書の内容はおもに word/document.xml
に記載されている。
今回のターゲットとなる部位の抜粋。
<w:p w:rsidR="00332438" w:rsidRDefault="00792A30"> <w:r> <w:rPr> <w:rFonts w:hint="eastAsia"/> </w:rPr> <w:t>こんにちは</w:t> </w:r> <w:r> <w:rPr> <w:rFonts w:hint="eastAsia"/> </w:rPr> <w:t xml:space="preserve"> </w:t> </w:r> <w:fldSimple w:instr=" MERGEFIELD hogehoge \* MERGEFORMAT "> <w:r> <w:rPr> <w:noProof/> </w:rPr> <w:t>«hogehoge»</w:t> </w:r> </w:fldSimple> <w:r> <w:rPr> <w:rFonts w:hint="eastAsia"/> </w:rPr> <w:t xml:space="preserve"> </w:t> </w:r> <w:r> <w:rPr> <w:rFonts w:hint="eastAsia"/> </w:rPr> <w:t>さん!</w:t> </w:r> <w:bookmarkStart w:id="0" w:name="_GoBack"/> <w:bookmarkEnd w:id="0"/> </w:p>
埋め込みを行う立場から見ると,ちょっとめんどい (あくまで ODF に比べての話だけど)。MergeField じゃないもっと適切なフィールドタイプがあるのかもしれない。
フィールド埋め込みについては Perl のワンライナーで書くなら以下のような感じ。
$ perl -pe 's{<w:fldSimple .*?> (.*? <w:t>) .*? (</w:t> .*?) </w:fldSimple> }{$1dayflower$2}gxms' document.xml
さきほどの ODF の例とは置換内容がちょっと違う (全埋め込みフィールド対象になっちゃってる) けど,ちゃんとやるなら置換後のほうにコード仕込んでとかやるかな。
OOXML でもやはり文書の統計情報が存在する。実態は docProps/app.xml
のいくつかのタグ。以下抜粋。
<Pages>1</Pages> <Words>9</Words> <Characters>52</Characters> <Lines>1</Lines> <Paragraphs>1</Paragraphs>
再構築
$ zip -r9 ../new2.docx * adding: [Content_Types].xml (deflated 75%) adding: _rels/ (stored 0%) ......
作成された文書を再度 Microsoft Word 2010 で開いてみたところ,きちんと埋め込みされていた。
同じ値を複数ヶ所に同時に埋め込みたい場合
もちろん全プレースホルダを上記のようにプログラムから書き換えてもいいんだけど,もっとスマートな解決策もある。OpenDocument などにはユーザ定義変数のようなものがあって,それを文書中に埋め込むことができる。この変数の値の部分だけ書き換えれば,文書中のそのユーザ変数フィールドは全部置き換わることになる。
以下は OpenDocument での例。面倒なので Microsoft Word ではどうやるか調べていないが,同等の機能はたぶんあると思う。
- 「挿入」メニューから「フィールド」「その他」メニューを選択する
- 「変数」タブを選択
- 「フィールドタイプ」で「ユーザー欄」を選択
- 「名前」フィールドに「変数名」を,「値」フィールドにデフォルトの値を入力
- (書式は「テキスト」を選択しておくのが無難)
これでユーザ定義変数が定義される (「選択」ペインにユーザ変数一覧が表示される)。
あとは文書中の挿入したい場所にカーソルを移動して「フィールド」の「挿入」ダイアログをさきほどと同じように出し,「選択」ペインで作成したユーザ定義変数を選択し,「挿入」ボタンを押下すれば埋め込まれる。
このときの content.xml
の内容は以下の通り。
<text:user-field-decls> <text:user-field-decl office:value-type="string" office:string-value="hogehoge" text:name="holder"/> </text:user-field-decls> <text:p text:style-name="Standard"> こんにちは <text:user-field-get text:name="holder">hogehoge</text:user-field-get> さん <text:user-field-get text:name="holder">hogehoge</text:user-field-get> さん <text:user-field-get text:name="holder">hogehoge</text:user-field-get> さん! </text:p>
さきほどと差しこみフィールドの実現のされ方が結構違っている。
変数部分 (<text:user-field-decl>
) のほうだけ書き換えればいいので,適当なワンライナーだけど (ほんとうなら text:name
のほうでチェックするべき)
$ perl -i -pe 's{string-value="hogehoge"}{string-value="dayflower"}gxms' content.xml
こんな感じで。
実態のドキュメント部分 (<text:user-field-get>
) のほうは置き換えてないけど,LibreOffice で再度開いた際には変数の埋めこまれた部分は自動的に変更されていた。ファイル自体はちょっと気持ち悪い状態だけど。
メモ
その他既存のアプローチについて (COM 以外)。
- Perl の場合
- OpenOffice-OODoc を使えば OpenDocument を読み・書き・操作できるみたい?そこそこメンテナンスされているようだ。
- Ruby の場合
- Java の場合
- そもそも UNO を使えば Java から OpenOffice.org, LibreOffice 等を操れるはず。
- Apache POI で OLE2 (旧来の MS Office document コンテナ) や OOXML のファイルを読み・書き・操作できるようだ。
- とはいえコアはあくまでコンテナの読み書きっぽい。各アプリケーション (Word など) の文書を操作する高次 API も用意されているようだが (例: Apache POI HWPF) クオリティについては不明。
winbind による Active Directory 認証 on Ubuntu 11.04
Ubuntu での winbind による Active Directory 認証は以前 winbind で Linux 認証 on Ubuntu - daily dayflower で書いたんだけど,だいぶ古くなったんで今の環境むけに。一応,対象 Natty だけど Maverick とかたぶん Lucid あたりまで適用できると思う。
面倒といえば面倒なのでそれを回避したいなら LikeWise 使うとか (第174回 Likewise OpenでActive Directoryを利用する:Ubuntu Weekly Recipe|gihyo.jp … 技術評論社 参照)。ただ LikeWise もカスタマイズされた Samba 等を内包してたりするんで,中身が見えないと嫌な人には向いてない。
諸元
Active Directory サーバ
- Windows 2000 Server
- Active Directory with SFU schema
2011年にもなるのに Windows 2000 Server なんてセキュリティの面とかいろいろ間違っている気がするけど,大人の事情。Windows Server 2003 以降なら SFU schema はいらない。というか 2000 Server でもシェル等が決め打ちでよかったりすれば SFU schema はいらない。
凡例
以前のを踏襲。
- ドメイン名
- HOGE
- Active Directory のレルム
- hoge.example.com
- Active Directory のドメインコントローラ
- dc.hoge.example.com
- クライアントホスト名
- Penguin
Kerberos の設定
デフォルトでは入っていないんで krb5-config をインストール。
$ sudo apt-get install krb5-config
以前は krb5-user もインストールしていたんだけど,今は kinit
しなくても net ads join
できるんでインストールする必要はないと思う。実際なくてもできた。
/etc/krb5.conf の抜粋:
[libdefaults] default_realm = HOGE.EXAMPLE.COM [realms] HOGE.EXAMPLE.COM = { kdc = dc.hoge.example.com admin_server = dc.hoge.example.com default_domain = hoge.example.com } [domain_realm] .hoge.example.com = HOGE.EXAMPLE.COM hoge.example.com = HOGE.EXAMPLE.COM
Samba, winbind の設定
samba, winbind をインストール。
$ sudo apt-get install samba winbind
/etc/samba/smb.conf の抜粋:
[global] workgroup = HOGE realm = HOGE.EXAMPLE.COM security = ADS obey pam restrictions = Yes algorithmic rid base = 10000 template homedir = /home/%U template shell = /bin/bash # winbind separator = ! winbind cache time = 60 winbind use default domain = Yes winbind nss info = sfu:HOGE winbind refresh tickets = Yes winbind normalize names = Yes winbind enum users = Yes winbind enum groups = Yes idmap uid = 10000-19999 idmap gid = 10000-19999 idmap backend = rid
winbind separator
を指定するとなぜか pam モジュールでの認証がうまくいかない。winbind separator
を指定した場合というか winbind normalize names
と併用した場合の気がする。なので泣く泣く (?) 指定をはずしている。ま,実際には使うことほとんどないし,DOMAIN\user
な指定はメジャー (かつ Windows 側との整合性もとれてる) んで泣く泣くというほどではない。ただ,いざ使うときはシェル等から使うことが多いからエスケープが必要だったりしてダサい。
Active Directory への参加とテスト
さきほど書いたとおり kinit
は必要ないんでいきなり net ads join
する。
$ sudo net ads join -U Administrator Administrator's password: ******** Using short domain name -- HOGE Joined 'PENGUIN' to realm 'hoge.example.com' No DNS domain configured for penguin. Unabled to perform DNS Update. DNS update failed!
以前書いたとおり,手元の環境では DNS をドメインコントローラに任せていないんで,上記のように怒られている。これでも問題なく使える。
きちんと join できてるかテスト((testjoin
は join の試行を行うのではなく,join がされているかどうか調べるコマンド。名前が悪い。))。
$ sudo net ads testjoin Join is OK
できてる。
いよいよ winbind を立ち上げる。
$ sudo service winbind start * Starting the Winbind daemon winbind ...done.
なぜか winbind だけは upstart 形式になってない。ので SysV init 系の service コマンドで起動 (もちろん /etc/init.d/winbind で実行してもいいけど)。
きちんと winbind が働いているか調べる。
$ wbinfo -t checking the trust secret for domain HOGE via RPC calls failed Could not check secret $ sudo wbinfo -t checking the trust secret for domain HOGE via RPC calls succeeded
一瞬焦ったが,wbinfo -t
に限っては root 権限でないと実行できないようだ。
Active Directory のグループ一覧を取得してみる (こちらはユーザ権限で可; ユーザ一覧は -u
オプション)。
$ wbinfo -g netshow administrators dhcp users dhcp administrators ...... (以下略)
nss の設定
winbind が起動しただけでは,Active Directory のユーザ情報は winbind 内で完結しており Linux から使えない。これを Linux のユーザ情報管理システムと結合するのが nss である。
/etc/nsswitch.conf の抜粋:
passwd: compat winbind group: compat winbind shadow: compat winbind
shadow:
んとこはいらない気もするけど一応。RHEL ではコマンドで設定できたけど,Ubuntu だと全部手書きしなくちゃなんない。
ついでに,Windows マシンの名前解決ができるように hosts に wins をいれておく。
hosts: files mdns4_minimal [NOTFOUND=return] dns wins mdns4
うまくユーザ情報を利用できるかテスト。
$ id dayflower uid=11190(dayflower) gid=10513(domain_users) 所属グループ=10513(domain_users),10512(domain_admins)
できてる。もちろん getent passwd とかでたしかめてもよい。
pam の設定
以上で Active Directory 上のユーザのアカウント (情報) が Linux のアカウントと結合された。だが,ログイン時等の認証は pam 経由でおこなっているのでそちらの設定をする必要がある。
設定に必要なファイルはインストールされているので pam-auth-update を実行するだけでよい。
$ sudo pam-auth-update
のだが,ホームディレクトリが存在していないユーザはログイン等できない。これでは不便なので,認証時にホームディレクトリが存在しない場合,自動的にホームディレクトリを作成するようにする。
ホームディレクトリの作成自体は pam_mkhomedir.so を使えばできる。これをさきほどの pam-auth-update コマンドで導入されるよう,下記のようなファイルを作成する。
/usr/share/pam-configs/mkhomedir :
Name: Automatic User Dir Generation Default: no Priority: 192 Session-Type: Additional Session: optional pam_mkhomedir.so skel=/etc/skel/ umask=0022
実は (いつごろからか忘れたけど) pam_winbind.so に mkhomedir
というオプションが用意され,それを使えばわざわざ pam_mkhomedir.so を利用する必要はなくなった。んでもそっちだと umask=
等のオプションはないし,システム提供の /usr/share/pam-configs/winbind を書き換える必要があるので敬遠してる。
pam の認証とは関係ないが,Domain Admins に所属するユーザは sudo できたほうがいいので,そのように設定する。
$ sudo visudo
/etc/sudoers の抜粋:
# Members of the admin group may gain root privileges %admin ALL=(ALL) ALL %domain_admins ALL=(ALL) ALL
winbind normalize names = Yes
という設定にしているので domain_admins
という指定になっている。普通に書くなら domain\ admins
。
あと,このままだと GNOME 等で音がでなかったり USB 機器が使えなかったりする可能性がある*1ので,/etc/group を編集して,ログインするユーザを各グループに適宜所属させている。人数が多くなると面倒 (だし一元管理の趣旨に反する) ので,その場合は /etc/security/group.conf を設定すればうまくできるのかな?ただ現状 (Natty 11.04) だと pam_group は login にしか記述されてないんでうまくいくのか不明。
起動スクリプト (upstart)
以上で Active Directory に登録されているユーザであればこの Linux マシンにログインできる。
そのためには winbind さえあがっていればよい。Windows マシンからこのホスト名 (例では Penguin) を名前解決するためには nmbd もあがっていたほうがよい。SMB 共有を行わないのであれば smbd はあがっている必要はない。
ということで起動時に smbd をあがらないようにしてみる (Ubuntu だと Samba をインストールすると smbd, nmbd が自動的に起動するようになっているので)。smbd, nmbd は upstart 経由でサービスコントロールされているので,下記のファイルをいじる (のだと思う。 upstart にくわしくないのでほんとうにこれでいいかわからない)。
/etc/init/smbd.conf の抜粋:
#start on (local-filesystems and net-device-up)
start on
というところをコメントアウトした。
あとは今現在あがっている smbd をおとす。
$ sudo initctl stop smbd
もちろん sudo stop smbd でもよい。
*1:惰性で /etc/group いじってしまってるんで,もしいじらなかった場合に使えないのかどうか実は知らない。
Java で RSA 暗号を使う
まず OpenSSL コマンドで RSA 秘密鍵を生成する。
$ openssl genrsa -out private_key.pem 1024 Generating RSA private key, 1024 bit long modulus ........................++++++ ...++++++ e is 65537 (0x10001)
末尾にビット数を指定している。OpenSSL のコマンド側だとたとえば 256 とかでも通るんだけど,これを Java のほうにもってくと
java.security.InvalidKeyException: RSA keys must be at least 512 bits long
のように怒られたりする。ただし Sun JRE 付属のプロバイダ (JCE) だとそうなだけで Bouncy Castle をプロバイダに使うと怒られなかった。
このあと openssl rsa
コマンドで DER 形式にすればいいだけかなと思ったらのちのち怒られたので PKCS #8 形式かつ DER 形式に変換する。
openssl pkcs8 -in private_key.pem -topk8 -nocrypt -outform DER -out private_key.pk8
公開鍵はふつうに DER 形式で出力すればいいだけ。
openssl rsa -in private_key.pem -pubout -outform DER -out public_key.der writing RSA key
以上のようにして OpenSSL で生成した鍵を使って Java で暗号化・復号化してみる。
なお,本来公開鍵暗号方式では,平文を公開鍵 (受信者側が公開した鍵) で暗号化し,暗号文を秘密鍵 (受信者側が秘匿している鍵) で復号化する。だが,今回の案件では秘密鍵 (送信者側が秘匿している鍵) で暗号化し,公開鍵 (送信者側が公開した鍵) で復号をおこなった。つまり RSA による暗号化というより半ば署名的に使ったことになる。通信内容の秘匿には使えないが,
- 第3者に暗号文を生成させたくない
- カジュアルに解読されなければいい
などの理由によりこのような仕様とした。
プログラムは以下のとおり。
package com.example.rsajava; import java.io.BufferedReader; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStreamReader; import java.security.GeneralSecurityException; import java.security.Key; import java.security.KeyFactory; import java.security.spec.KeySpec; import java.security.spec.PKCS8EncodedKeySpec; import java.security.spec.X509EncodedKeySpec; import javax.crypto.Cipher; import org.apache.commons.codec.binary.Base64; public class Main { private static final String CIPHER_ALGORITHM = "RSA"; private static final String CIPHER_MODE = CIPHER_ALGORITHM + "/ECB/PKCS1PADDING"; private static final String PRIVATE_KEY_FILE = "private_key.pk8"; private static final String PUBLIC_KEY_FILE = "public_key.der"; public static void main(String[] args) { Main app = new Main(); InputStreamReader input = new InputStreamReader(System.in); BufferedReader reader = new BufferedReader(input, 1); for (;;) { System.out.print("INPUT: "); System.out.flush(); String line; try { line = reader.readLine(); if (line == null) break; } catch (IOException e) { e.printStackTrace(); break; } byte[] encrypted = app.encryptWithPrivateKey(line.getBytes()); System.out.print("ENCRYPTED: "); System.out.println(Base64.encodeBase64String(encrypted)); byte[] decrypted = app.decryptWithPublicKey(encrypted); System.out.print("DECRYPTED: "); System.out.println(new String(decrypted)); System.out.println(); } } private Cipher cipher; private Key secret_key; private Key public_key; public Main() { try { cipher = Cipher.getInstance(CIPHER_MODE); KeyFactory keyfactory = KeyFactory.getInstance(CIPHER_ALGORITHM); KeySpec keyspec; keyspec = new PKCS8EncodedKeySpec(loadBinaryFile(PRIVATE_KEY_FILE)); secret_key = keyfactory.generatePrivate(keyspec); keyspec = new X509EncodedKeySpec(loadBinaryFile(PUBLIC_KEY_FILE)); public_key = keyfactory.generatePublic(keyspec); } catch (GeneralSecurityException e) { throw new RuntimeException(e); } } public byte[] encryptWithPrivateKey(byte[] source) { try { cipher.init(Cipher.ENCRYPT_MODE, secret_key); return cipher.doFinal(source); } catch (GeneralSecurityException e) { throw new RuntimeException(e); } } public byte[] decryptWithPublicKey(byte[] source) { try { cipher.init(Cipher.DECRYPT_MODE, public_key); return cipher.doFinal(source); } catch (GeneralSecurityException e) { throw new RuntimeException(e); } } private static byte[] loadBinaryFile(String filename) { try { FileInputStream in = new FileInputStream(filename); byte[] data = new byte[in.available()]; in.read(data); in.close(); return data; } catch (IOException e) { e.printStackTrace(); } return new byte[0]; } }
かなり RSA 暗号のコード - BiBoLoG からパクっている。Base64 形式の出力のため Apache Commons Codec を利用している。あと例外処理は適当すぎる。
以下実行例。
INPUT: Hello, World! ENCRYPTED: gswm4jxzVI7QDM+EFqYq+uzniO81FsGfZVYsGtsecD22TPw+AqB33QKE7WzP0+fKiIPcCjyz/eJL Z6bm+mplpY5I7QZkIOD+rtW443YaXULU/DXTf0kb/pPMOSbyHMLF5hWZJbzJSq7iGOFuyWqxZ9DB JvlpobhKF9DLZGJ3JJ0= DECRYPTED: Hello, World! INPUT: Hello, World! ENCRYPTED: gswm4jxzVI7QDM+EFqYq+uzniO81FsGfZVYsGtsecD22TPw+AqB33QKE7WzP0+fKiIPcCjyz/eJL Z6bm+mplpY5I7QZkIOD+rtW443YaXULU/DXTf0kb/pPMOSbyHMLF5hWZJbzJSq7iGOFuyWqxZ9DB JvlpobhKF9DLZGJ3JJ0= DECRYPTED: Hello, World! INPUT: hello, World! ENCRYPTED: NR7vRLEkRHLpUwFzmgS2lf8FwkpUiX0HGnv6rCgx9wOc2SFupXVog3+7AJmGZlq7RA24LrRKCjsS awIwtiyPaPDVtT+MYgmb5DWcV9fivewygvw5Ct6qRmvjUMl26+uglYkQ0yF6g250rp8NOJa1k5l/ 0NBx4e7ls5Q2C7drHjw= DECRYPTED: hello, World!
(少なくとも Java での) RSA 暗号化には初期ベクタがないので,同じ文から同じ暗号文が生成されている (1 つめの例と 2 つめの例)。しかし 3 つめの例から少なくとも先頭の文字が違っていれば暗号文の全体 (というかブロック長) が変化するので,自分で初期化ベクタを用意してやればカジュアルに傍受している側をまどわせることができると思われる。
ほか注意点
KVM で PXE ブートしようとしたら難儀した
KVM の場合,virt-install
コマンドに --pxe
オプションをつければ PXE ブートさせることができる。Xen と違って完全仮想化だから。ばんざい。
# virt-install --name=hogehoge --accelerate --os-variant=rhel5.4 \ --ram=384 --vcpus=1 \ --file=hogehoge.img \ --network=bridge:br0 --pxe \ --vnc --noautoconsole
あるいは qemu-kvm コマンドを直接叩いて network option rom を指定するという方法もあるようだ (参考, qemu-kvm の直接起動は Stray Penguin - Linux Memo (KVM-2) 参照)。
これで普通に DHCP サーバと TFTP サーバを構成していればうまく PXE ブートできるはずなのだが,手元の環境では以前 Proxy DHCP サーバで PXE ブート - daily dayflower で書いたように Proxy DHCP を使っている。
この Proxy DHCP 環境下で KVM を PXE ブートさせようとしたらなかなかうまくいかなかった。
結論からいうと,KVM (Qemu) で利用しているネットワークコントローラ用の ROM (Etherboot) にバグがあった。
ちなみに Etherboot 由来のネットワークブート ROM は Oracle VirtualBox でも使っている。ので同様に Proxy DHCP 環境ではうまく PXE できない (かった)。
原因 1: Proxy DHCPOFFER の NBP file name に対応していない
Proxy DHCP サーバで PXE ブート - daily dayflower でも書いたように,PXE_DISCOVERY_CONTROL Option の bit 3 を立てると DHCPOFFER の段階でブートファイル名を返すことができる。
ところが,Etherboot 由来の PXE BIOS はこのオプションを無視する。そして UDP 4011 宛に DHCPREQUEST を投げてくる。
しかたないので pxe-pdhcp を改造して 68 番と 4011 番両ポートで待ち受けするように変更した。
原因 2: Etherboot の ProxyDHCP 時の DHCPREQUEST パケットがおかしい
これで一件落着かと思ったら,DHCPREQUEST のベンダオプション項がうまくパースできない。仕様の読み漏れかと思って Etherboot のサイトからソースを落として読んだら,バグがあった。ベンダオプション項の最後に End of Options (FFh) をつけ忘れている。
Etherboot 4.4.2 でのパッチは以下のとおり。
diff -r e53c2fc7ea6a src/core/nic.c --- a/src/core/nic.c Wed Feb 16 12:19:14 2011 +0900 +++ b/src/core/nic.c Wed Feb 16 14:07:59 2011 +0900 @@ -1166,6 +1166,7 @@ /* Construct the ProxyDHCPREQUEST packet */ memcpy(ip.bp.bp_vend, rfc1533_cookie, sizeof rfc1533_cookie); memcpy(ip.bp.bp_vend + sizeof rfc1533_cookie, proxydhcprequest, sizeof proxydhcprequest); + ip.bp.bp_vend[sizeof rfc1533_cookie + sizeof proxydhcprequest] = RFC1533_END; for (reqretry = 0; reqretry < MAX_BOOTP_RETRIES; ) { printf ( "\nSending ProxyDHCP request to %@...", arptable[ARP_PROXYDHCP].ipaddr.s_addr); udp_transmit(arptable[ARP_PROXYDHCP].ipaddr.s_addr, BOOTP_CLIENT, PROXYDHCP_SERVER,
報告しようと思ったらバグトラッカも落ちてるしレポジトリもみつからない。どうもサイトが故障していたようだ。一部サービス (ソース提供や rom-o-matic での NIC ROM 自動生成) はすでに復旧している。
一応上記のパッチを適用してビルドした ROM ではうまく PXE boot できた((NIC ROM の置き換えは,/usr/share/kvm
以下の pxe-virtio.bin
などを置き換える。もともとは /usr/share/qemu-pxe-roms/
以下の ROM ファイルへのシンボリックリンク。))。だが,前述したように,VirtualBox でも問題のある Etherboot 由来の NIC ROM を使っており,こちらをどのように差し替えるかがわからない。なので,pxe-pdhcp 側で workaround をおこなった。
ほんとは workaround のソースを示せればいいんだけど,諸般の事情*1でまだだせない。あくまで Etherboot のバグ対策だけだが,
- DHCP Options の Class Identifier が 32 バイト続いたあとで,(本来
FFh
があるべきだが) 「:UNDI:〜
」とゴミが続いてしまっている - よって
pdhcp.c
のcheck_dhcp_packet()
において DHCP Option が3Ah
,55h
の並びになった場合,そこを終端とみなすようにする
という対策をおこなったところ,CentOS 5.5 の KVM guest および VirtualBox 4.0.2 において PXE ブートでできるようになった。
余談
Etherboot プロジェクトは実質 gPXE プロジェクトが後継となっているようだ。こちらのソースは一から書きおこしている部分が多く,上記のようなバグはない (なさそうだった)。
だが gPXE の NIC ROM を生成して KVM で使ってみたところ,なぜか主 DHCP サーバ宛に TFTP 要求をだしていた。ソース上は ProxyDHCP 用のコードもあるようだがまだきちんとインプリメントされていないのかもしれない。
なお,Etherboot では上記のパッチを適用して自分で ROM をビルドしたが,gPXE の NIC ROM は http://rom-o-matic.net/gpxe/gpxe-1.0.1/contrib/rom-o-matic/ で NIC ROM を生成した。いろいろ条件を指定して NIC ROM を生成できるのでおもしろいサイトである。
ちなみに,Etherboot (と gPXE) では,条件を指定することで TFTP だけでなく,HTTP 経由でイメージをとってこれる NIC ROM も作成できるようだ。また,gPXE のほうは iSCSI による SAN Boot にも対応しているらしい (id:adsaria さんによる実践例)。
Debian 6.0 Live イメージを PXE 経由でブート
さいきんの Debian は Live 起動用イメージも用意しているらしい。
んで,Live 起動イメージはネットワークブート用のイメージも用意されている。
たとえば,http://ftp.jaist.ac.jp/pub/Linux/Debian-CD/6.0.0-live/i386/net/ をみるといくつかの種類 (GNOME 環境とか Rescue 環境とか) のネットブート用 Live イメージ (*.tar.gz) がおいてあることがわかる。
そこで,CentOS で作った PXE ブート環境に,選択肢のひとつとして Debian 6.0 Live を追加してみた。index.rb [nofuture.tv] が参考になる。
なお PXE 環境自体の構築については今回触れない。
ライブイメージディストリビューションのなかから debian-live-6.0.0-i386-standard.tar.gz を選択してみた。standard といいつつほとんど何も入ってないプレーンな状態だけど。
取得して展開すると,だいたい下記のようなディレクトリツリーになってる。
+ debian-live/ + tftpboot/ + tftpboot/debian-live/ + tftpboot/debian-install/
おおまかにいうと,tftpboot/
フォルダ以下が TFTP による初期ブートに必要なファイル群 (カーネルとか initramfs とか)。debian-live/
は initramfs が起動したあとに NFS 経由で読み込むルートイメージ。NFS 経由といっても生ルートツリーを NFS マウントするわけじゃなくてルートイメージが SquashFS 形式でおいてあるんでそれを NFS 経由で読み込んでマウントする感じ。
なお tftpboot/debian-install/
のほうは,Debian をネットワークインストールする際に使う TFTP ブートイメージで,今回は使用しなかった。
NFS 用ルートイメージの用意
前述のとおり debian-live/
以下が NFS 経由のルートイメージになるので適当な場所において NFS で公開する。
# cp -R debian-live /pub/linux/debian/squeeze
別にディレクトリはこの名前にする必要があるわけじゃない。自分の環境では上記のようにしているだけ。
上記例だと exports
はこんな感じ。
# cat /etc/exports /pub/linux/debian *(ro,all_squash,no_subtree_check,crossmnt) /pub/linux/debian/squeeze *(ro,all_squash,no_subtree_check,crossmnt)
んで NFS デーモンを再起動。
# service nfs restart
PXE用ファイルの用意
こんどは TFTP ブート用ファイルを /tftpboot/
以下の適当なディレクトリにコピー。
# cp -R tftpboot/debian-live /tftpboot/debian-squeeze
んで,Debian のドキュメントとかだと付属する pxelinux.cfg
をそのまま使うような形で紹介されてるんだけど,そうするといままで使用していた他の PXE ブート環境がなくなってしまってもったいないので要所だけコピーする。
Debian Live の場合,たとえば debian-live/i386/boot-screens/live.cfg
に PXE ブート用の設定が書いてある。
label live menu label Live kernel debian-live/i386/vmlinuz-2.6.32-5-486 append initrd=debian-live/i386/initrd.img-2.6.32-5-486 boot=live config netboot=nfs nfsroot=192.168.1.1:/srv/debian-live quiet
改行いれちゃってるけど,append 行のとこは1行で書く必要があると思う (以下も同様)。これをもとに,自分の pxelinux.cfg
に書き足す。
今回の例示だと以下のような内容。/tftpboot/
の下とか NFS 用ディレクトリに好き勝手な名前をつけたので,対応する部分を変更してある。
LABEL squeezelive MENU LABEL Debian 6.0 Live KERNEL debian-squeeze/i386/vmlinuz-2.6.32-5-486 APPEND initrd=debian-squeeze/i386/initrd.img-2.6.32-5-486 boot=live config netboot=nfs nfsroot=<NFS Server Address>:/pub/linux/debian/squeeze quiet
以上で無事 Debian 6.0 Squeeze の Live 環境が立ち上がった。あまりにプレーンなのでほんとにうまく動いてるか自信がないので今 GNOME 版の Live イメージをダウンロード中*1。
なお,Ubuntu でも同じようにすれば Live 環境の PXE 起動はできる。つっても違うところはいろいろあって
- TFTP ブート用のファイルは netboot.tar.gz というのをとってくる
- NFS 用ルートイメージは,Ubuntu インストール DVD をマウントしたものを NFS エクスポートすればそのまま使える
pxelinux.cfg
まわりは preseed とか casper まわりとかをAPPEND
行でいろいろ指定する必要がある
いろいろたいへんそうだけど,たいていの人は Ubuntu のライブイメージを手元に持ってるだろうから,ハードルは Debian より高くない。
Ubuntu の場合の pxelinux.cfg
例を下記においとく。
LABEL mavericklive MENU LABEL Ubuntu 10.10 Live KERNEL ubuntu-maverick/i386/casper/vmlinuz APPEND initrd=ubuntu-maverick/i386/casper/initrd.lz file=ubuntu-maverick/i386/preseed/ubuntu.seed boot=casper netboot=nfs nfsroot=<NFS Server Address>:/pub/linux/ubuntu/maverick quiet splash --
TFTP 用ファイルは /tftpboot/ubuntu-maverick/
においており,NFS 用ルートイメージが /pub/linux/ubuntu/maverick/
においてあるという前提だけど。当然ながら KERNEL 行や APPEND 行のファイルパスは適宜書き換えること。
羽田空港から台北いってきた - 風景編
日本ともそう遠くは変わらない風景*1。
路地裏。表の顔と裏の顔の分離。奥のトラックはギリギリじゃないか。
選挙
行った時がたまたま選挙シーズンだったのか,選挙ののぼりとか各所にあった。
日本と違うなと思ったところ。
- 名前の前に番号がついてる。この番号でも投票できるのかな?
- (政治家として) 若手の女性はルックス重視っぽい
- 巨大な広告もうてるみたい
- 下の写真ではそんなに大きくないけど,結構巨大なビル広告とかもあった
- あと CHANGE とか広告に書いてる候補者もいたww
日本的もの
日本的ってわけじゃないけど,あちらにもこういう落書き?グラフティ?ありました。どこの国もかわらんなー。
麻布茶房。こんなポップな雰囲気だっけ?ともかく,台湾滞在中に2店舗くらい見かけた気がする。
あと,和民ね。広告の雰囲気とかもおんなじ気がする。料金は日本と同じくらいなんだけど,これはつまり台湾の物価からすると高い部類ですね。
エラー
街中の端末のエラー画面に惹かれてしまうのはなぜでしょうか。
これは MRT の券売機のエラー。エラー内容はよくわかんないや。
こちらはエバー航空の自動チェックインマシン。エラーってわけじゃないけど,ふつーの Windows の画面になっちゃってる。
その他
あるお店に入ったときにあった電子カレンダー。珍しいのでお願いして撮らせてもらった。左側は旧暦かな?
整形の看板。「人工進化女神」というフレーズにひかれて撮った。
迪化街 (という問屋街) の光景なんだけど,おばちゃんの自転車のうしろかごにヨウムが止まってる!よく逃げないな!
*1:ここは別に繁華街ではないです。繁華街はもっともっとちゃんと栄えてます。
羽田空港から台北いってきた - 食事編
食事は全般的に日本人の舌に合ってると思う。ただ八角 (スターアニス) が苦手だとつらいかも。といっても小籠包は八角の香りはしないので大丈夫。
明月湯包
たぶん支店のほうに行ったのかな。そっちのほうが広くて賑わってる印象だったんで。
タクシー移動に飽きたというのもあり,MRT で移動。MRT 六張犁駅の近くに放射路があるんだけど,その信号をいくつか渡っていかないといけないのね。んで夜ということもあり見事にいくべき道を間違えて難儀しました。MRT からいくなら,すくなくとも駅でて放射路を渡り切るとこまでの詳細な地図はもってたほうがいいかも。
こういう亜流はあんまりおいしくないかなと想像してたけど,普通の小籠包に飽きた舌には新鮮で,ノーマルよりおいしく感じた。
別に牛肉麺の名店ってことはないんだけど,清燉牛肉麺たのんだら珍しくスープと麺がべつべつにでてきた。やさしい味で美味だった。
店入って右手のワゴンに惣菜の入った小皿がならんでた。台北ナビのクーポンでもらえる野菜一品というのは,この惣菜です。クーポン適用には500元以上注文する必要あり。軽食つまむ感覚だとなかなか500元までいかないです。
阜杭豆漿
台北の食事処としては珍しく,フードコートに入ってるお店。華山市場の2F。市場といっても小規模デパートみたいな感じだけど。かなり混んでいるので,フードコートの空き席はなくはないけど大人数の場合覚悟が必要かも。
こんな感じで地元の人がすんごい並んでる。観光客もいるけどあんまりいない。んで次々に注文がさばかれていくので,日本人としては料理の名前を書いて見せるのが一番トラブルが少ないんじゃないかと思う。
伝統のあるお店なんだけど,小奇麗なガラス張りの厨房。
鹹豆漿と油條を注文。油條は選択ミスだった気がする。鹹豆漿にもともと少し入ってるなんて知らなかったんだもん。油條の代わりに厚餅夾蛋を注文したかった。
鹹豆漿は味の濃い茶碗蒸しのような手作り豆腐のような (豆乳が凝固してるんだから当たり前か) 優しい味。
度小月
日本語メニューありで安心。
店内はすごく綺麗で日本のおしゃれ居酒屋的内装 (メニューもそんな感じ)。だけど,料理の値段はとても安い。
入口はいってすぐのところでおっさんが座って担仔麺をひたすら作ってる (これは2階から撮った)。
あと汁ありコリアンダーありの担仔麺も頼んだんだけど,コリアンダーの味がすごく支配的でまるでベトナムのブンのようだった。汁ありなしにかかわらず,コリアンダーなしで注文したほうが料理そのものの味をきちんと味わえていいかも。
丸林魯肉飯
ホテルからとても近かったんでいっただけ。ということであんまり期待してなかったけど,今回の旅行で一番のアタリだったかも。
お昼時にいったら1階は地元の人たちで芋洗い状態。
1階はビュッフェ形式で小皿に盛ってくれる。なので見た目から判断して身振り手振りで何皿か盛ってもらう。んで最後の会計のところで魯肉飯と絶叫してたら2階にいけといわれた。なので小皿料理をもって2階へ。2階は1階と違ってすいていて,ある程度の日本語が通じるおばちゃんがいた。観光客だと2階に通されるのかな。
ここの魯肉飯は大と小の2種類があるんだけど,この写真は「大」。「大」でも小ぶりの茶碗サイズなので「小」を頼む必要はないかと思われる。
味は,個人的にはカレーヌードルにご飯をつっこんだときのような味のように感じた。別にカレー粉は入ってないと思うけど,ジャンクな味って意味で。店員には「そぼろごはん」といわれたけど,そぼろほども肉々しくない。
蛤のスープ。想像通りの味だけど,とても胃に優しい味でほっとする。しょうがが入っているのがまたおいしい。
百果園
- 【陳記百果園】Jason's Fruits
- オフィシャル。ジャムやドライフルーツをオンラインショップで買うことも,一応?,できる。送料が高くついちゃうけど。
- 百果園[バイグォユェン] | 台湾グルメ・レストラン-台北ナビ
カウンター後ろの黒板にはメニューが綺麗な日本語でも書いてある。なので日本人びいきの店かとおもいきや,あたる店員によってはかなり無愛想。
ハズレの店員にあたったらしく,マンゴーかき氷を指さすも NO の返事 (たしかに季節的に NG だった)。じゃあってんでフルーツ盛り合わせかき氷指さすも NO。じゃあジェラートならいいかなと,店員と相談しながらマンゴーとドラゴンフルーツを選択。
どーんとおっきいのが来た (ピントがあってなくてすいません)。結果的にフルーツ盛り合わせかき氷+ジェラート盛り合わせを注文したことになってたらしい。無愛想なのはネガティブじゃなくて言葉が全然通じないからだったのね。反省。
これ写真じゃ大きさはわかんないかもだけど,小ぶりのボウルくらいの器に,これでもかっ,てくらいフルーツが詰め込まれてた。親子3人だと食べきるのにも一苦労。
脆皮鮮奶甜甜圈
ドーナツ屋さん。
もともと晴光市場 (雙城夜市の開かれるあたり) に出来た店なんだけど,ホームページによると台北車站店もできたみたい。台北車站と中山駅のあいだくらい。
ホテルが近かったので晴光店で買った。ので台北車站店が実在するかはわかんない。
ドーナツ自体の写真を撮るの忘れたー。ドーナツはかなり大きいです。ミスドとかの2倍位の大きさはあるんじゃないかな。味は,皮サクサクで,まぁ普通においしいです。
台北市街東エリア (市政府のほうとか) に宿泊してる人がわざわざ晴光市場まで出張って買う価値があるかどうかまではわかんないですが,台北車站店が存在してるなら,なにかのついでにそこで買うというのはありかも。