Subscribed unsubscribe Subscribe Subscribe

OpenIDMをちょこっと触ってみました

今年はOSSのIdMがくる!? と期待もこめてOpenIDMを紹介。年末年始に空き時間を使ってちょこっとさわってみました。

IdMとは

IdMについてはOracleの記事が分かり易かったのでこちらを紹介。

OpenIDMは上記の説明でいうとIDライフサイクル管理を担当します。Oracleの製品でいうとOracle Identity Managerの位置に相当するソフトウェアですね。

OpenIDMとは

OpenAMを開発しているForgeRock社によるIdMソフトウェア。OpenAMと同じくこちらもOSSでライセンスはCDDL。

OpenAMは元々SunがOpenSSOとして開発していて、Oracleに買収されたあと元Sunの人たちがForgeRock社を立ち上げてOpenSSOをForkして開発されています。一方、OpenIDMはベースはなくスクラッチで開発されている模様(参考)。ただし、外部リソースとのコネクタ部分はSunがOSSで開発していたIdentity Connector FrameworkをForkしたOpenICFを採用している(OpenICFもForgeRockで開発されている)。

最新安定板はバージョン2.0.3。次バージョン2.1は予定ではXpress版がもうでているはずなのだがまだ出ていない。。。
ロードマップはこちら

動かしてみる

お試しなので最新安定板ではなく、2.1.0 Experimental Nightly 版を使用してみた。

OSSなのに意外?とドキュメントがあり、インストール&サンプルを動かすチュートリアル(OpenIDM 2.1.0 Installation Guide)がしっかり書かれているので、簡単に動かせます。英語だけど、平易な英語で書かれていて分かり易い。

OpenIDM 2.1.0 Installation Guideの目次にサンプル一覧がある。LDAPSQL系は別途環境を用意しないといけないが、それ以外はOpenIDM単体ですぐに試せます。

3.2. Sample 1 - XML File
3.3. Sample 2 - LDAP One Way
3.4. Sample 2b - LDAP Two Way
3.5. Sample 2c - Synchronizing LDAP Group Membership
3.6. Sample 2d - Synchronizing LDAP Groups
3.7. Sample 3 - Scripted SQL
3.8. Sample 4 - CSV File
3.9. Sample 5 - Synchronization of Two Resources
3.10. Sample 6 - LiveSync Between Two LDAP Servers
3.11. Sample 7 - Scripting a SCIM-like Schema
3.12. Sample 8 - Logging in Scripts
3.13. Sample 9 - Asynchronous Reconciliation Using Workflows

さくっと動かしたい人は、Sample 1、Sampl 4、Sample 5がおすすめです。

  • Sample 1 : XMLで書かれたユーザー情報をOpenIDMのRepositoryに同期させるサンプル。プロビジョニング先はなし。
  • Sample 4 : CSVで書かれたユーザー情報をOpenIDMのRepositoryに同期させるサンプル。これもプロビジョニング先はなし。
  • Sample 5 : XMLで書かれたユーザー情報をOpenIDMのRepositoryに同期させつつ、別のXMLファイルにプロビジョニングするサンプル。

Sampleごとにコンフィグファイルが用意されており、起動時にオプションでディレクトリを切り替えればすぐに試せます。例えばSample 1を試したければ、OpenIDMを下記のように実行すればOK。

cd /path/to/openidm
./startup.sh -p samples/sample1

OpenIDMのアーキテクチャ

Integrator's GuideのChapter 1. Architectural Overviewを参照。2010年頃からフルスクラッチで開発されているのでモダンな作りになっている。

気になった特徴は以下。

  • REST API
    • 基本的にRESTでHTTP経由で操作するようになっている。他システムとの連携がやりやすそうです。
  • Script Engineによる柔軟さ
    • OpenIDM自体はJavaで開発されているが、Script Engine(Rhino)が動いておりJavaScriptで色々と定義するようになっている。例えば、ユーザーの姓、名をあるシステムに連携(プロビジョニング)する際は、「姓 名」と連結しないといけない場合など、JavaScriptで以下のように定義できる。
  "target": "displayName",
  "source": "";
  "transform": {
      "type": "text/javascript",
      "source": "(source.lastName +', ' + source.firstName;)"
  }
  • 2種類の同期
    • ReconciliationとLiveSyncの2種類の方法がある。
      • Reconciliationは機能的には差分同期。ソース(同期元)とターゲット(同期先)のデータを比較して変更点を認識して結果を反映。
      • LiveSyncはソースのチェンジログをもとに同期する方法。Reconciliationと異なり差分をチェックする必要がないので軽量。ただし、チェンジログはOpenIDMが認識できる形でないとだめ。現在のバージョンだとOpenDJとActiveDirectoryに対応しているみたい。
    • また、直接OpenIDMのRepository(ManagedUser)をRESTで更新してやれば、即座に他のリソースに反映することも可能です。リアルタイム同期をしたい場合はREST APIをたたく方がいいのかも。

REST APIでユーザー登録・更新

Linuxからならcurlコマンドで簡単にできる。HTTPヘッダでログイン情報を渡す必要がある。ボディでJSON形式でデータを渡してあげる。

  • RESTで登録
curl --header "X-OpenIDM-Username: openidm-admin" --header "X-OpenIDM-Password: openidm-admin" --request PUT --data '{ "email":"fdoe@example.com","familyName":"Doe","userName":"fdoe", "givenName":"Felicitas","displayName":"Felicitas Doe" }' "http://localhost:8080/openidm/managed/user/fdoe"


  • RESTで更新
    • header "If-Match: *" を追加しているのがポイント。
curl --header "If-Match: *" --header "X-OpenIDM-Username: openidm-admin" --header "X-OpenIDM-Password: openidm-admin" --data '{"email":"fdoe@example.com","familyName":"Doe","userName":"fdoe", "givenName":"Felicitas","displayName":"Felicitas Doe"}' --request PUT "http://localhost:8080/openidm/managed/user/fdoe"


  • RESTで更新(特定属性のみ指定)
    • PATCHで特定属性のみ更新することもできる。
curl --header "If-Match: *" --header "X-OpenIDM-Username: X-OpenIDM-Password: openidm-admin" --data '[{"replace":"givenName","value":"Felicitas Changed!"}]' --request PATCH "http://localhost:8080/openidm/managed/user/fdoe"

または

curl --X-OpenIDM-Password: openidm-admin" --data '[{"replace":"givenName","value":"Felicitas Changed!"}]' --request POST "http://localhost:8080/openidm/managed/user/fdoe?_action=patch"

おまけ:性能もちょっとみて見た

MacBook AirでVirtual Boxを動かし、CentOS6.2で確認。メモリ512MB割り当ててます。CPUは1つのみ。

RepositoryはデフォルトのOrientDBからMySQLに変更しています。Chapter 4. Installing a Repository For Productionに変更方法が書かれています。

Reconciliationで差分同期させてかかった時間を確認してみました。

  • CSV->ManagedUser(MySQL)->OpenLDAP
    • 1000件(追加): 135秒
    • 1000件(更新なし): 56秒
    • 1000件(更新1000件): 124秒
  • OpenLDAP->ManagedUser(MySQL)->OpenLDAP
    • 1000件(追加): 104秒
    • 1000件(更新なし): 35秒
    • 1000件(更新1000件): 101秒

topで見るとjavaプロセス(つまりOpenIDM)の処理がボトルネックのようでほとんど使い切っていました。内部的にはマルチスレッドで処理されているようなので、CPUを増やせば速くなるかも??
他の製品を触ったことがないのでこれが遅いのか速いのか何とも言えませんが。。。
個人的な感覚としてはちょっと遅いようにも思う。1000件LDAPインポートだけだと一瞬ですしね。JavaScriptの評価が遅いのかも? RhinoじゃなくてV8あたりにすれば速くなったりしないかな〜

ChefでデフォルトのAttributesを動的に設定する方法

ChefのAttributesで悩んだことがあったのでメモ。結論から言うと、AttributeファイルではできなくてRecipe内で定義するしかなさそう。

説明のために、Chefで構築するソフトウェアが以下のディレクトリ構造であるとする。

/opt
 +-sample   ・・・インストール先のベースディレクトリ
    +-bin 
    +-data   ・・・仮想のソフトウェアのデータディレクトリ
    +-logs   ・・・仮想のソフトウェアのログ出力ディレクトリ

このソフトウェアのインストール先、データディレクトリ、ログ出力先をAttributesでパラメタ化する。Attributeを素直に書くとこうなる。

  • sample/attributes/default.rb
default[:sample][:base_dir] = "/opt/sample"
default[:sample][:data_dir] = "/opt/sample/data"
default[:sample][:logs_dir] = "/opt/sample/logs"

しかしこれだと、base_dir を変えたときに合わせて data_dir、logs_dir も変えないといけない。例えばRoleでデフォルトのAttributeを上書きする際に、以下のように全部定義しないといけない。

  • role/sample.rb
name "sample"
description "Sample App"
default_attributes(
  :sample => {
    :base_dir => "/usr/local/sample",
    :data_dir => "/usr/local/sample/data",
    :logs_dir => "/usr/local/sample/logs"
  }
)
run_list(
  "recipe[sample]"
)

これは面倒なので、base_dir の値を data_dir、logs_dir に埋め込んで動的に設定されるようにしたくなる。よくあるAttributeの書き方だと以下のように#{node[...]}で埋め込む方法になっている。

  • sample/attributes/default.rb
default[:sample][:base_dir] = "/opt/sample"
default[:sample][:data_dir] = "#{node[:sample][:base_dir]}/data"
default[:sample][:logs_dir] = "#{node[:sample][:base_dir]}/logs"

しかしこれではうまく機能しないケースがある。デフォルト設定なら問題ないのだが、例えば下記のようにRoleで base_dir だけを変更したとする。

  • role/sample.rb
name "sample"
description "Sample App"
default_attributes(
  :sample => {
    :base_dir => "/usr/local/sample"
  }
)
run_list(
  "recipe[sample]"
)

期待する動作としては、Roleで設定した base_dir + /data, /logs なのだがそうはならない。data_dir, logs_dir は相変わらずデフォルト設定のままとなり変更されないのだ。Environmentを使用した場合も同じです。どうも、Attributesが評価される段階ではまだRoleやEnvironmentで設定した値は反映されないらしい(参考)。

結論

動的に決定させたいAttributeはRecipeで設定するしか現状なさそうです。Recipe内であればRoleやEnvironmentで設定した値がnodeに反映されている。

下記のように、動的な部分はAttributeでは定義しないようにする。

  • sample/attributes/default.rb
default[:sample][:base_dir] = "/opt/sample"

代わりにRecipe内で設定する。

  • sample/recipes/default.rb
default_unless[:sample][:data_dir] = "#{node[:sample][:base_dir]}/data"
default_unless[:sample][:logs_dir] = "#{node[:sample][:base_dir]}/logs"

ここでdefault_unlessを使用するのがミソ。これだと未設定の場合だけ設定してくれる。こうしておくと、下記のようにRoleで設定した値が優先されるし、定義しなければデフォルト値が使用される。例のように、logs_dir だけ /var/log 以下に変更したい、という場合に使える。

  • role/sample.rb
name "sample"
description "Sample App"
default_attributes(
  :sample => {
    :logs_dir => "/var/log/sample/logs"
  }
)
run_list(
  "recipe[sample]"
)

しかし、パラメータをAttributeファイルに全て集約できないので見通しが悪くなってしまうのがちょっといまいちですね〜。Attributeファイル内で使用した#{node[...]}の評価を後で実行してくれればいいのですが・・・。

参考

上記を見るとapply_expansion_attributes(expand!) をAttributeファイルのトップにかけばうまくいく、という話もあるが、Chefの実装依存の動きらしいのでオススメできないそうです(もううまく動かないという話もあり)。

Chef ServerをCentOS6.3にインストールする

http://wiki.opscode.com/display/chef/Installing+Chef+Server+using+Chef+Solo

を参考に、Chef ServerをCentOS6.3にインストールした。一部エラーが発生しつまづいたのでメモっておきます。

なお、rubyrubygems、その他必要なライブラリはCentOS標準のものをyumでインストールし、chef-soloはgemで最新(10.16.0)をインストール済みの環境です。

遭遇したエラー

chef-solo -c /etc/chef/solo.rb -j ~/chef.json -r http://s3.amazonaws.com/chef-solo/bootstrap-latest.tar.gz

の実行で以下のエラーが出る。

Generated at Tue Oct 23 20:13:15 +0900 2012
Mixlib::ShellOut::ShellCommandFailed: service[chef-expander] (chef-server::rubygems-install line 219) had an error: Mixlib::ShellOut::ShellCommandFailed: Expected process to exit with [0], but received '1'
---- Begin output of /sbin/service chef-expander start ----
STDOUT: chef-expander を起動中: [失敗]
STDERR: /usr/lib/ruby/site_ruby/1.8/rubygems.rb:233:in `activate': can't activate eventmachine (~> 0.12.10, runtime) for ["chef-expander-10.16.0"], already activated eventmachine-1.0.0 for ["amqp-0.6.7", "chef-expander-10.16.0"] (Gem::LoadError)
        from /usr/lib/ruby/site_ruby/1.8/rubygems.rb:249:in `activate'
        from /usr/lib/ruby/site_ruby/1.8/rubygems.rb:248:in `each'
        from /usr/lib/ruby/site_ruby/1.8/rubygems.rb:248:in `activate'
        from /usr/lib/ruby/site_ruby/1.8/rubygems.rb:1082:in `gem'
        from /usr/sbin/chef-expander:18
---- End output of /sbin/service chef-expander start ----
Ran /sbin/service chef-expander start returned 1
/usr/lib/ruby/gems/1.8/gems/mixlib-shellout-1.1.0/lib/mixlib/shellout.rb:248:in `invalid!'
/usr/lib/ruby/gems/1.8/gems/mixlib-shellout-1.1.0/lib/mixlib/shellout.rb:234:in `error!'
/usr/lib/ruby/gems/1.8/gems/chef-10.16.0/bin/../lib/chef/mixin/shell_out.rb:36:in `shell_out!'
/usr/lib/ruby/gems/1.8/gems/chef-10.16.0/bin/../lib/chef/provider/service/init.rb:53:in `start_service'
/usr/lib/ruby/gems/1.8/gems/chef-10.16.0/bin/../lib/chef/provider/service.rb:90:in `action_start'
/usr/lib/ruby/gems/1.8/gems/chef-10.16.0/bin/../lib/chef/mixin/why_run.rb:63:in `call'
/usr/lib/ruby/gems/1.8/gems/chef-10.16.0/bin/../lib/chef/mixin/why_run.rb:63:in `converge!'
/usr/lib/ruby/gems/1.8/gems/chef-10.16.0/bin/../lib/chef/mixin/why_run.rb:61:in `each'
/usr/lib/ruby/gems/1.8/gems/chef-10.16.0/bin/../lib/chef/mixin/why_run.rb:61:in `converge!'
/usr/lib/ruby/gems/1.8/gems/chef-10.16.0/bin/../lib/chef/provider.rb:136:in `converge'
/usr/lib/ruby/gems/1.8/gems/chef-10.16.0/bin/../lib/chef/provider.rb:125:in `run_action'
/usr/lib/ruby/gems/1.8/gems/chef-10.16.0/bin/../lib/chef/resource.rb:593:in `run_action'
/usr/lib/ruby/gems/1.8/gems/chef-10.16.0/bin/../lib/chef/runner.rb:49:in `run_action'
/usr/lib/ruby/gems/1.8/gems/chef-10.16.0/bin/../lib/chef/runner.rb:81:in `converge'
/usr/lib/ruby/gems/1.8/gems/chef-10.16.0/bin/../lib/chef/runner.rb:81:in `each'
/usr/lib/ruby/gems/1.8/gems/chef-10.16.0/bin/../lib/chef/runner.rb:81:in `converge'
/usr/lib/ruby/gems/1.8/gems/chef-10.16.0/bin/../lib/chef/resource_collection.rb:94:in `execute_each_resource'
/usr/lib/ruby/gems/1.8/gems/chef-10.16.0/bin/../lib/chef/resource_collection/stepable_iterator.rb:116:in `call'
/usr/lib/ruby/gems/1.8/gems/chef-10.16.0/bin/../lib/chef/resource_collection/stepable_iterator.rb:116:in `call_iterator_block'
/usr/lib/ruby/gems/1.8/gems/chef-10.16.0/bin/../lib/chef/resource_collection/stepable_iterator.rb:85:in `step'
/usr/lib/ruby/gems/1.8/gems/chef-10.16.0/bin/../lib/chef/resource_collection/stepable_iterator.rb:104:in `iterate'
/usr/lib/ruby/gems/1.8/gems/chef-10.16.0/bin/../lib/chef/resource_collection/stepable_iterator.rb:55:in `each_with_index'
/usr/lib/ruby/gems/1.8/gems/chef-10.16.0/bin/../lib/chef/resource_collection.rb:92:in `execute_each_resource'
/usr/lib/ruby/gems/1.8/gems/chef-10.16.0/bin/../lib/chef/runner.rb:80:in `converge'
/usr/lib/ruby/gems/1.8/gems/chef-10.16.0/bin/../lib/chef/client.rb:378:in `converge'
/usr/lib/ruby/gems/1.8/gems/chef-10.16.0/bin/../lib/chef/client.rb:420:in `do_run'
/usr/lib/ruby/gems/1.8/gems/chef-10.16.0/bin/../lib/chef/client.rb:176:in `run'
/usr/lib/ruby/gems/1.8/gems/chef-10.16.0/bin/../lib/chef/application.rb:140:in `run_chef_client'
/usr/lib/ruby/gems/1.8/gems/chef-10.16.0/bin/../lib/chef/application/solo.rb:224:in `run_application'
/usr/lib/ruby/gems/1.8/gems/chef-10.16.0/bin/../lib/chef/application/solo.rb:216:in `loop'
/usr/lib/ruby/gems/1.8/gems/chef-10.16.0/bin/../lib/chef/application/solo.rb:216:in `run_application'
/usr/lib/ruby/gems/1.8/gems/chef-10.16.0/bin/../lib/chef/application.rb:72:in `run'
/usr/lib/ruby/gems/1.8/gems/chef-10.16.0/bin/chef-solo:25
/usr/bin/chef-solo:19:in `load'
/usr/bin/chef-solo:19

解決策

chefのメーリングリストに解決方法があり。

http://lists.opscode.com/sympa/arc/chef/2012-09/msg00333.html

eventmachine の 1.0.0をアンインストールし、chef-soloでのインストールを再実行すれば成功した。アンインストールは下記コマンドを実行し、1.0.0の削除を選ぶ。

gem uninstall eventmachine

SphinxのTipsを紹介

Sphinxでマニュアルを書き始めてノウハウがちょっとずつたまってきたので、Tipsを書いていこうと思います。整理してまとめて書くと時間がかかり陳腐化しそうなので、書けた部分から小出しで紹介していきます。

includeディレクティブで画像も含めて取り込む方法

Sphinxを採用した理由の1つとして、複数のマニュアルを書く際に共通部分は二重管理したくないという点がありました(参考)。ソフトウェアのいわゆるDRY (Don't repeat yourself) 原則と同じように、ドキュメントでも重複を避けたいというのが大きな理由の1つでした。

Sphinxだと、includeディレクティブを使うことで他のドキュメントを取り込んで再利用することができます。

例)

.. include:: ../common/reusable.rst

課題

includeディレクティブの残念なところとして、figureディレクティブで指定した画像まではうまく扱えないという問題があります。

例えば、Sphinxで作成するドキュメントソースが以下の体系だったとします。ドキュメントがdoc1、doc2の二種類あり、それぞれで共通の文章reusable.rstを含めるようなケースを想定しています。

source
 +-- common
 |    +-- reusable.rst  … chapter1, 2で両方使いたい
 |    +-- sample.png    … reusable.rst内でfigureディレクティブを使い埋め込んでいる画像
 +-- doc1
 |    +-- chapter1.rst  … includeディレクティブでcommon/reusable.rstを含める
 +-- doc2
      +-- chapter1.rst  … includeディレクティブでcommon/reusable.rstを含め

reusable.rstでは、以下のようにfigureディレクティブを使いsample.pngを埋め込んでいるとします。

.. figure:: sample.png
	:scale: 100%
	:width: 500px

これでHTMLを生成しても、残念ながら画像は埋め込まれません。reusable.rstはdoc1、doc2のchapter1.rstにそれぞれマージされてから、figureディレクティブの処理が行われる動きとなっており、doc1、doc2ディレクトリ以下にsample.pngがないと画像が埋め込まれません。

試しに、doc1、doc2ディレクトリ以下にsample.pngを配置してあげるとうまく表示されるようになります。ですがこれだと全くうれしくありません。reusable.rstはDRYにできているのに、画像ファイルは二重管理になってしまいます。

解決策

解決策としては、例えば「ビルドスクリプトで頑張る」のと「Sphinxを拡張する」方法があると思います。

ビルドスクリプトで頑張る

この方式は、単にSphinx実行前に画像ファイルをコピーするという方法です。しかし、再利用元のディレクトリはどこなのかは作成するドキュメント次第なので、ビルドスクリプトのメンテナンスが都度入りそうですしこの方式は避けたいところです。また、同一名の画像ファイルがもしあると単純コピーでは上書きしてしまうので、その考慮も必要になります。

Sphinxを拡張する

もう1つはSphinxを拡張して対応するという方式です。Sphinxではさまざまなレベルで拡張できるように設計がされています(参考:Sphinx拡張)。

今回のケースだと、他にも色々やり方があるかもしれませんが、標準のincludeディレクティブを拡張して対応することができます。

実は、figureディレクティブで画像ファイル名を指定していますが、絶対パスで指定してあげるとincludeディレクティブでもうまく扱えます。例えば、以下のように書きます
*1

.. figure:: //workspace/sample/source/common/sample.png
	:scale: 100%
	:width: 500px

絶対パスで書くことで、includeディレクティブでマージされても画像ファイルにアクセス可能なので大丈夫、というわけです*2

しかし、元からこのような絶対パス方式で書いておくと、ビルド環境が変わった途端に動かなくなるのでオススメできません。また、相対パスでも書くことができるのですが、先ほどの例のように再利用元全てでディレクトリ階層が同じなら問題ないですが、異なると使えません。

というわけで、従来のincludeディレクティブを拡張して、実行時に自動的に絶対パスに展開するという処理を追加してあげれば、うまく対応できそうです。

インストールされているSphinxのオリジナルのincludeディレクティブのソースを直接修正しても対応できますが、ビルド環境依存になってしまうので、今回は拡張モジュールを作成し、Sphinxの拡張モジュールとして登録して対応しました。そうすればソースリポジトリに一緒に格納することができますので、どのSphinx環境でも同じように動かすことが可能になります。

拡張モジュールの設定方法は以下のとおり。

  • source/_extentions/ 以下に拡張モジュールのpyファイルを配置 (例えば、ext_include.pyを配置)
  • conf.pyのextensionsに配置したモジュールを追加
extensions = ['sphinx.ext.todo', 'sphinx.ext.ifconfig', 'ext_include']

ext_include.pyは、docutils-0.9-py2.6のdocutils/parsers/rst/directives/misc.pyをコピーして作成しました。変更点は以下の通りです。追加処理内容としては、.. figure:: を検知して絶対パスに置換しているだけです。
後は、def setup(app)を追加して、ディレクティブとしてSphinxに登録させるようにしています。ディレクティブ名をincludeとし、オリジナルのincludeを上書きしています。

diff --git a/source/_extentions/ext_include.py b/source/_extentions/ext_include.py
index cd371e2..e5fc4cc 100644
--- a/source/_extentions/ext_include.py
+++ b/source/_extentions/ext_include.py
@@ -18,6 +18,10 @@
 from docutils.parsers.rst.roles import set_classes
 from docutils.transforms import misc
 
+def setup(app):
+    app.add_directive('include', Include)
+
+
 class Include(Directive):
 
     """
@@ -60,6 +64,9 @@
         if path.startswith('<') and path.endswith('>'):
             path = os.path.join(self.standard_include_path, path[1:-1])
         path = os.path.normpath(os.path.join(source_dir, path))
+        
+        abspath = path
+        
         path = utils.relative_path(None, path)
         path = nodes.reprunicode(path)
         encoding = self.options.get(
@@ -149,369 +156,14 @@
                                   self.state,
                                   self.state_machine)
             return codeblock.run()
+
+        # convert to abspath
+        for index, line in enumerate(include_lines):
+            if '.. figure:: ' in line:
+                include_lines[index] = re.sub(r':: +', ':: ' + os.path.dirname(abspath).replace('\\', '/') + '/', line)
+                print 'convert figure path to abspath.'
+                print 'Before: ' + line
+                print 'After: ' + include_lines[index]
+
         self.state_machine.insert_input(include_lines, path)
         return []

これで画像ファイルも含めて再利用することができるようになります。

*1:先頭にスラッシュ(/)が2つないとLinuxでは絶対パスとみなされないため、わざとこのように記述しています

*2:絶対パスで定義してもHTML出力時には相対パスに変換されるので大丈夫です

丸の内MongoDB勉強会に参加してきた

丸の内MongoDB勉強会 : ATND に参加してきました(17時から開始はさすがに間に合わず30分ほど遅刻...)。
MongoDBは初めて触りましたが、各プラットフォームのバイナリも用意されていて、インストールも展開するだけと簡単。初心者に優しいですね。これなら簡単に勉強を始めれそうだ。実際、プログラミング経験がほとんど無い方も参加されていたのだが、ハンズオンでMongoDBを動かせていたようだし。

しかし私は使用していたPCからネットワークの問題かgemがうまく動かず、MongoDB-Ruby driverを使用したサンプルを動かせず。。。というわけでSphinxビルド用にPythonを入れていたのでそちらで挑戦してみた。

PythonにもRubyのようにクライアントライブラリがあるようで、それを使えばほとんど同じようなコードで書けますね。

import pymongo
import re
import urllib2

title = re.compile(r'<h1>(.*)</h1>')
author = re.compile(r'<tr><td>By:</td><td><b>(.*)</b></td></tr>')
year = re.compile(r'<tr><td>Year:</td><td><b>(.*)</b></td></tr>')
page = re.compile(r'<tr><td>Paperback:</td><td><b>(.*) pages</b></td></tr>')
size = re.compile(r'<tr><td>File size:</td><td><b>(.*)</b></td></tr>')
format = re.compile(r'<tr><td>File format:</td><td><b>(.*)</b></td></tr>')
url = re.compile(r'href=\"/go/(.*?)[\'\"]')

def parse_html(html):
	scan = lambda r: r.search(html).group(1)

	return {
		"title": scan(title),
		"author": scan(author),
		"year": scan(year),
		"page": scan(page),
		"file": {
			"size": scan(size),
			"format": scan(format),
			"url": "http://it-ebooks.info/go/" + scan(url)
		}
	}


conn = pymongo.Connection()
db = conn.bookgetter
coll = db.book
coll.remove()

for i in range(1, 11):
	html = urllib2.urlopen("http://it-ebooks.info/book/%s/" % i).read()

	book_data = parse_html(html)
	# print book_data

	coll.insert(book_data)

Solr 3.6.1をJBoss AS 7.1.1.Final(Standalone)で動かしてみた

ちょっとSolrの動作確認をしたくなったため、手元にある7.1.1.Finalで動かしてみた。
Standaloneモード試しています。

環境

  • CentOS 6.3
  • JDK 1.7.0_05-b05
  • JBoss AS 7.1.1.Final
  • Solr 3.6.1

参考

http://wiki.apache.org/solr/SolrJBoss#JBoss_7_and_Solr_4.0-ALPHA
https://community.jboss.org/message/643825#643825

ポイントはSolrの設定ファイルやインデックス格納ディレクトリをJBoss ASに伝える方法かな。

Solrの入手&解凍

Solr3.6.1のダウンロードサイトより取得する。

取得したapache-solr-3.6.1.tgzを適当なディレクトリに解凍する。

Solrの設定をサンプルからコピー

今回はサンプルの設定ファイルを使用して稼働確認する。解凍したディレクトリにあるexample/confディレクトリごと任意のディレクトリにコピーする。

mkdir -p /path/to/solr_home
cp -r apache-solr-3.6.1/example/solr/conf /path/to/solr_home

JBoss ASのstandalone.xmlを編集

$JBOSS_HOME/standalone/configuration/standalone.xml を修正する。タグを追加しています。

        <extension module="org.jboss.as.webservices"/>
        <extension module="org.jboss.as.weld"/>
    </extensions>

    <system-properties>
        <property name="solr.solr.home" value="/path/to/solr_home"/>
        <property name="solr.data.dir" value="/path/to/solr_data"/>
        <property name="org.apache.catalina.connector.URI_ENCODING" value="UTF-8"/>
    </system-properties>
  • solr.solr.homeにはSolrのconfディレクトリをコピーしたディレクトリを設定する。
  • solr.data.dirには、任意のディレクトリを指定する。なお、この設定がない場合はsolr.solr.home/data ディレクトリが自動的に使われるのでなくても良い。
  • URI_ENCODINGをUTF-8に設定しないと、日本語での検索ができなくなるので注意。
  • org.apache.catalina.connector.USE_BODY_ENCODING_FOR_QUERY_STRING のtrue設定は不要で良い。これをtrueにすると日本語検索が逆にできなくなるようです。

※なお、JBoss ASの起動オプションで-Dsolr.solr.home=/path/to/solr_home のように設定するのもOK。

solr.warをデプロイ

apache-solr-3.6.1/example/webapps/solr.war をJBoss ASのデプロイディレクトリにコピーすればOK。

cp apache-solr-3.6.1/example/webapps/solr.war $JBOSS_HOME/standalone/deployments/

JBoss ASの起動

cd $JBOSS_HOME/bin
./standalone -b 0.0.0.0 &

とりあえず検証用に直でアクセスするので、-b 0.0.0.0を付けて外部からアクセス可能に指定しています。

Solrの管理画面にアクセス

http://192.175.204.145:8080/solr/admin/ にアクセスして管理コンソールが見えればOK。

とりあえずここまで。

GradleでMavenリポジトリにデプロイする時のメモ

しばらく書いていないと忘れてしまうのでメモ。
Gradle(Groovy)なので柔軟に書けちゃうので、これが正解というわけではないです。

基本的な設定

java, mavenプラグインを設定する。

apply plugin: 'java'
apply plugin: 'maven'

pom.xmlのgroupId、artifactId、versionの元ネタを設定する。archiveBaseNameは省略可能。その場合、プロジェクトのフォルダ名が使われる(参考)。archivesBaseNameの代わりに、jar.baseName でもOK。

group = 'com.github.wadahiro'
archivesBaseName = 'maven-upload-sample' 
version = '1.0'

uploadArchivesタスクを設定する。

uploadArchives {
    repositories {
        mavenDeployer {
            repository(url: 'file:/path/to/myrepo')
        }
    }
}

これで、gradle uploadArchivesを実行すると/path/to/myrepo 以下にmaven-upload-sample-1.0.jarがデプロイされるようになる。

HTTPでリポジトリにデプロイする

URLをhttp://〜に変更する。

uploadArchives {
    repositories {
        mavenDeployer {
            repository(url: 'http://mycompany/content/repositories/thirdparty')
        }
    }
}

認証付きの場合は以下のようにする。MavenリポジトリにNexusを使うとデフォルトで認証付きなので以下のようにauthenticationでユーザーID・パスワードを設定する。

uploadArchives {
    repositories {
        mavenDeployer {
            repository(url: 'http://mycompany/content/repositories/thirdparty') {
                authentication(userName: "admin", password: "admin123")
            }
        }
    }
}

ソースファイル、JavaDocもアップロードする

まず以下のタスクを追加し、*-sources.jar・*-javadoc.jarが生成されるようにする。

task sourcesJar(type: Jar, dependsOn:classes) {
     classifier = 'sources'
     from sourceSets.main.allSource
}

task javadocJar(type: Jar, dependsOn:javadoc) {
     classifier = 'javadoc'
     from javadoc.destinationDir
}

さらに、これらをartifactとして認識させるために、archiveタスクで定義したsourcesJar・javadocJarを指定する。
artifactについて詳細はChapter 44. Publishing artifactsを参照のこと。

artifacts {
     archives sourcesJar
     archives javadocJar
}

これで、gradle uploadArchivesを実行すると*-sources.jar・*-javadoc.jarが生成され、リポジトリにデプロイされるようになる。

POMのプロジェクト情報を設定する

Mavenだとpom.xmlにライセンスなどプロジェクトの情報を記述できるようになっています。gradleからもこれらは設定は可能。
以下のように書いておくと、gradle uploadArchivesによるデプロイ、gradle installによるローカルMavenリポジトリ($HOME/.m2/repository)へのインストール時に生成されるpom.xmlに反映されるようです。
ちょっと長ったらしいのでもっと良い書き方がないか探索中...

[install.repositories.mavenInstaller, uploadArchives.repositories.mavenDeployer]*.pom*.whenConfigured { pom ->
    pom.project {
        inceptionYear '2012'
        packaging 'jar'
        licenses {
            license {
                name 'The Apache Software License, Version 2.0'
                url 'http://www.apache.org/licenses/LICENSE-2.0.txt'
                distribution 'repo'
            }
        }
    }
}

POMの依存関係

build.gradleで設定した依存関係は、自動生成されるpom.xmlにも自動反映される。
依存関係の制御については別途書く予定。

最終的なbuild.gradle

apply plugin: 'java'                                                                                                                             
apply plugin: 'maven'

group = 'com.github.wadahiro.gradle-samples'
archivesBaseName = 'maven-upload-sample'
version = '1.0'

uploadArchives {
    repositories {
        mavenDeployer {
            repository(url: 'file:/path/to/myrepo')
        }
    }
}

task sourcesJar(type: Jar, dependsOn:classes) {
     classifier = 'sources'
     from sourceSets.main.allSource
}

task javadocJar(type: Jar, dependsOn:javadoc) {
     classifier = 'javadoc'
     from javadoc.destinationDir
}

artifacts {
     archives sourcesJar
     archives javadocJar
}

[install.repositories.mavenInstaller, uploadArchives.repositories.mavenDeployer]*.pom*.whenConfigured { pom ->
    pom.project {
        inceptionYear '2012'
        packaging 'jar'
        licenses {
            license {
                name 'The Apache Software License, Version 2.0'
                url 'http://www.apache.org/licenses/LICENSE-2.0.txt'
                distribution 'repo'
            }
        }
    }
}