tracの最近のブログ記事
pukiwikiにGraphvizプラグインというのがある。
graphvizはdot言語(?)で記述した構造を画像化するソフトだ。これをwikiのプラグインから使うというもの。 wikiで編集したdot言語が図に置き換わる。
ここのブログの以下のエントリで使っている図はgraphvizで描いたものだ。

という画像が生成される。日本語も問題ない。
dot言語については本家かGraphviz チュートリアルがわかりやすい。
graphvizはdot言語(?)で記述した構造を画像化するソフトだ。これをwikiのプラグインから使うというもの。 wikiで編集したdot言語が図に置き換わる。
ここのブログの以下のエントリで使っている図はgraphvizで描いたものだ。
導入
- graphvizとlibrsvg2をインストールする。FC3、FC5の場合yumなりでインストールできちゃうはず。
librsvg2はSVG出力しないか、pngでアンチエイリアスを使わない場合不要っぽいが、うちの環境では何かの依存関係ですでにインストールされていた。 - GraphvizPluginをダウンロードする。
今回使用したのは0.9 - ダウンロードしたアーカイブをどこか適当な場所に展開する。
- 展開先/graphvizplugin/0.9/ というディレクトリがあるはずなのでcdし「python setup.py bdist_egg」
展開先/graphvizplugin/0.9/dist/graphviz-0.6.9-py2.3.egg というファイルが出来たはずなので、tracプロジェクトのplugins/ディレクトリにコピーする。 細かいことはREADMEに書いてある。 - Graphviz Pluginはdotから生成した画像を保存する場所(=IMGのSRCのリンク先)が必要なのでこれを用意する。
tracdもしくはwebサーバから書き込み可能にしなければならない。環境に応じて適宜。
うちの環境では以下のディレクトリを使うことにした。tracプロジェクト/htdocs/graphviz/
- trac.iniに[graphviz]セクションを追加する。
うちの環境では以下の通り。他にもいろいろ設定できるが環境によって必須なのはgraphvizなどのコマンドのpath。[graphviz]
cache_dir = tracプロジェクト/htdocs/graphviz
png_antialias = 1 - ここの環境ではtracdを使っているので再起動した。
{{{
#!graphviz
digraph G {
graph [rankdir="LR" size="6,4"]
t1 [ label="明日は" ]
t2 [ label="明日の" ]
t3 [ label="風が" ]
t4 [ label="吹く" ]
t5 [ label="吹けば" ]
t6 [ label="桶屋が" ]
t7 [ label="儲かる" ]
t1 -> t2 -> t3 -> t4
t3 -> t5 -> t6 -> t7
a->b->c
}
}}}

という画像が生成される。日本語も問題ない。
dot言語については本家かGraphviz チュートリアルがわかりやすい。
tracから送られてくるメールのDateフィールドの値は、タイムゾーンがGMTになっている。
元のソースは、インタアクト株式会社が日本語化したtrac-0.9.5-ja-1。
Date: Sun, 16 Jul 2006 14:29:21 -0000タイムゾーンを無視しているメーラーがある。メーラーというかwebメールなのだが。あきらかにメーラーが悪いが、かといって9時間ずれたメールが並んでいるのはなんだか嫌だ。ということで、送信メールのDateフィールドにlocaltimeを適用するパッチを追加。
Date: Sun, 16 Jul 2006 23:32:00 +0900ダウンロード→trac.patch.txt
元のソースは、インタアクト株式会社が日本語化したtrac-0.9.5-ja-1。
> 元のソースを展開する(で、いけるはず)
> パッチをDLしてどこかに置く
> cd trac-0.9.5-ja-1/ (展開したソースディレクトリにcd)
> patch -p1 < DLしたパッチ
# python setup.py install
設定例
[notification]
always_notify_owner = true
smtp_always_cc =
smtp_password =
smtp_enabled = true
smtp_replyto = xxxxxxx@example.com
smtp_port = 25
always_notify_reporter = true
smtp_server = SMTPサーバ
smtp_from = xxxxxxx@example.com
smtp_user =
use_localtime = true
以下のエントリで書いたパッチをひとくくりにしたもの。
ダウンロード→trac.patch.txt
元のソースは、インタアクト株式会社が日本語化したtrac-0.9.5-ja-1。
ダウンロード→trac.patch.txt
元のソースは、インタアクト株式会社が日本語化したtrac-0.9.5-ja-1。
> 元のソースを展開する(で、いけるはず)
> パッチをDLしてどこかに置く
> cd trac-0.9.5-ja-1/ (展開したソースディレクトリにcd)
> patch -p1 < DLしたパッチ
# python setup.py install
trac.iniでsmtp_enabledをtrueにsmtp_fromかsmtp_replytoにアドレスを設定すれば、でチケットを登録したり更新した際に関係者にメールが送られる。
このメールについて、自分の趣味とは合わないな~と思っていること。
そもそもSubjectにプロジェクト名が入っているので、メールの一覧表示における情報量に無駄があるなあ、と。
※ちなみに、tracから来たメールはプロジェクトごとに特定のフォルダに振り分けて、スレッド表示にしている。
このメールについて、自分の趣味とは合わないな~と思っていること。
その1 - From:の表示名とアドレス
tracからのメールは以下のように「プロジェクトさん」から送られてきたことになる。From: [trac]セクションのname値 <[notification]セクションのsmtp_from値>一人でTODOやアイデア管理に使っている分には特に気にならないが、複数の利用者が1つのチケットに対してあれこれ変更や追記をすると、受信したメールの発信者が全て「プロジェクトさん」のため、メーラで整理しにくい。
そもそもSubjectにプロジェクト名が入っているので、メールの一覧表示における情報量に無駄があるなあ、と。
※ちなみに、tracから来たメールはプロジェクトごとに特定のフォルダに振り分けて、スレッド表示にしている。
- From:フィールドの表示名を報告者の名前にする
- といっても、on/offの制御はしたいところなので、trac.iniの[notification]セクションにuse_reporter_name=trueを設定すると、表示名が報告者の名前になるようにする。falseか未設定なら元の通りプロジェクト名が表示名になる。
- 「報告者の名前」は、以下の優先順位で適用される
- 「ユーザ設定」で「名前」に記入した文字列
- 認証名(ログイン状態で「xxxxとしてログイン中」と出るアレ)
- From:フィールドのメールアドレスを報告者のアドレスにする
- 表示名と同様に、trac.iniの[notification]セクションにuse_reporter_email=trueを設定すると、Fromアドレスが報告者のアドレスになるようにする。falseか未設定なら元の通りプロジェクト名が表示名になる。
- 結果的にFromの詐称を行うことになるので、smtpサーバによってはメール送信を蹴ってしまうかもしれない。(自ドメイン以外のメールアドレスがFrom:にあると蹴るケースなど)
- smtp_fromに指定したアドレスはMAIL FROMとしてsmtpセッションで使われ、結果的にSender:の値になる。ウチで使っている環境に関して言うと、smtp_fromにsmtpサーバにとってローカルなメールアドレスを指定しておけば、From:が他ドメインでも送信してくれた。
その2 - From:の表示名とあて先の表示名をMIMEエンコードしない
特にanonymousで使っていて、「ユーザ設定」で日本語の名前を設定している場合、表示名部分をMIMEエンコードしないので文字化けしてしまう。先のエントリにも書いたようにウチの環境はanonymousでは使っておらず、認証名も英字のみとしているので、あまり問題ないのかも。- 送信先が「表示名 <アドレス>」の場合、表示名部分をMIMEエンコードする。判定はかなり適当。
- 文字セットはSubjectを踏襲してUTF-8として、手抜きした。pythonでiso-2022-jpに変換するのって簡単なのかな。
その3 - Message-ID:の生成の仕方
プロジェクトのURLやチケットのIDを用い、ハッシュをとったもの+αにsmtp_fromの@以降を付与している。smtpサーバに外部のサーバを使用している場合にどうも気持ち悪い。smtp_fromがxxxxxxx@example.comだと、
Message-ID: <071.56dd0b824d3625a8ce2b492402b465db@example.com>
となる。
Message-ID: <071.56dd0b824d3625a8ce2b492402b465db@tracサーバのFQDN>
としたい。
- trac.iniの[notification]セクションで、use_fqdn4message_id=trueに設定した場合、Message-ID:の@以降をtracをホストしているサーバのFQDNにする。
- falseか未設定なら従来通りsmtp_fromの@以降。
今回のパッチ
--- trac-0.9.5-ja-1/trac/Notify.py 2006-02-16 10:47:21.000000000 +0900
+++ trac-0.9.5-ja-1kai/trac/Notify.py 2006-06-25 00:07:56.000000000 +0900
@@ -20,9 +20,12 @@
from trac.web.clearsilver import HDFWrapper
from trac.web.main import populate_hdf
+import re
+import socket
import md5
import time
import smtplib
+from email.Header import Header
class Notify:
@@ -74,6 +77,8 @@
server = None
email_map = None
template_name = None
+ reporter_name = ''
+ reporter_email = ''
def __init__(self, env):
Notify.__init__(self, env)
@@ -117,6 +122,17 @@
if self.user_name:
self.server.login(self.user_name, self.password)
+ def modifyNotification(self , authname , reporter_name_ , reporter_email):
+ if self.env.config.getbool('notification', 'use_reporter_name'):
+ if reporter_name_:
+ self.reporter_name = reporter_name_
+ elif authname:
+ self.reporter_name = authname
+
+ if self.env.config.getbool('notification', 'use_reporter_email'):
+ if reporter_email:
+ self.reporter_email = reporter_email
+
def send(self, rcpt, mime_headers={}):
from email.MIMEMultipart import MIMEMultipart
from email.MIMEText import MIMEText
@@ -132,7 +148,18 @@
msg['X-Trac-Project'] = projname
msg['X-URL'] = self.config.get('project','url')
msg['Subject'] = Header(self.subject, 'utf-8')
- msg['From'] = '%s <%s>' % (projname, self.from_email)
+
+ display_name = projname
+ from_email_ = self.from_email
+ if self.env.config.getbool('notification', 'use_reporter_name') and \
+ self.reporter_name:
+ display_name = self.reporter_name
+
+ if self.env.config.getbool('notification', 'use_reporter_email') and \
+ self.reporter_email:
+ from_email_ = self.reporter_email
+
+ msg['From'] = '%s <%s>' % (Header(display_name, 'utf-8'), from_email_)
msg['Sender'] = self.from_email
msg['Reply-To'] = self.replyto_email
msg['To'] = rcpt
@@ -301,6 +328,11 @@
emails = []
for recipient in recipients:
if recipient.find('@') >= 0:
+ p = re.compile('([^<]*)\s*(<.+>)')
+ m = p.match(recipient)
+ if m:
+ recipient = '%s %s' % \
+ (Header(m.group(1), 'utf-8'), m.group(2))
emails.append(recipient)
elif self.email_map.has_key(recipient):
emails.append(self.email_map[recipient])
@@ -312,12 +344,20 @@
result.append(e)
return result
+ def get_message_id_host(self):
+ if self.config.getbool('notification', 'use_fqdn4message_id'):
+ fqdn = socket.getfqdn()
+ if fqdn:
+ return fqdn
+
+ return self.from_email[self.from_email.find('@') + 1:]
+
def get_message_id(self, rcpt, modtime=0):
"""Generate a predictable, but sufficiently unique message ID."""
s = '%s.%08d.%d.%s' % (self.config.get('project', 'url'),
int(self.ticket.id), modtime, rcpt)
dig = md5.new(s).hexdigest()
- host = self.from_email[self.from_email.find('@') + 1:]
+ host = self.get_message_id_host()
msgid = '<%03d.%s@%s>' % (len(s), dig, host)
return msgid
--- trac-0.9.5-ja-1/trac/ticket/web_ui.py 2006-02-16 10:59:33.000000000 +0900
+++ trac-0.9.5-ja-1kai/trac/ticket/web_ui.py 2006-06-24 22:28:52.000000000 +0900
@@ -139,6 +139,8 @@
# Notify
try:
tn = TicketNotifyEmail(self.env)
+ tn.modifyNotification(req.authname, \
+ req.session.get('name', None), req.session.get('email', None))
tn.notify(ticket, newticket=True)
except Exception, e:
self.log.exception("Failure sending notification on creation of "
@@ -347,6 +349,8 @@
try:
tn = TicketNotifyEmail(self.env)
+ tn.modifyNotification(req.authname, \
+ req.session.get('name', None), req.session.get('email', None))
tn.notify(ticket, newticket=False, modtime=now)
except Exception, e:
self.log.exception("Failure sending notification on change to "
設定例
[notification]あとは、SubjectかどこかでチケットのStatusがわかるといいんだけど、と思っているがとりあえずここまで。
always_notify_owner = true
smtp_always_cc =
smtp_password =
smtp_enabled = true
smtp_replyto = xxxxxxx@example.com
smtp_port = 25
always_notify_reporter = true
smtp_server = SMTPサーバ
smtp_from = xxxxxxx@example.com
smtp_user =
use_reporter_name = true
use_reporter_email = true
use_fqdn4message_id = true
前回のパッチで、「tracdによるtracプロジェクトをhttpsでapacheの認証下(basic/digest以外)で公開する。そしてapacheで認証したユーザでtracにログインする」というところまで出来た。
tracプロジェクトでanonymousを許す場合、apacheの認証を「/login」にだけに設定しておけばよい。だけど、自分の環境では「tracプロジェクト全域」に認証をかけているため、事実上anonymousを利用することはない。したがって、一度認証を通過しているにも関わらずいちいち「ログイン」を選択しないとログイン状態にできないのは面倒だ(特に、チケットの発行や「私のチケット」を見るとき)
ということで、tracプロジェクト全域を認証して使う場合に、いちいち"ログイン"を選ばなくてもいいようにする。
ちなみに、権限をdropしてanonymousで利用できなくする、というのとは目的が違うので注意。
tracプロジェクトでanonymousを許す場合、apacheの認証を「/login」にだけに設定しておけばよい。だけど、自分の環境では「tracプロジェクト全域」に認証をかけているため、事実上anonymousを利用することはない。したがって、一度認証を通過しているにも関わらずいちいち「ログイン」を選択しないとログイン状態にできないのは面倒だ(特に、チケットの発行や「私のチケット」を見るとき)
ということで、tracプロジェクト全域を認証して使う場合に、いちいち"ログイン"を選ばなくてもいいようにする。
ちなみに、権限をdropしてanonymousで利用できなくする、というのとは目的が違うので注意。
- 以下のパッチをあてる。
--- trac-0.9.5-ja-1/trac/web/auth.py 2006-01-09 16:48:09.000000000 +0900
常にログインした状態で利用させるのでログアウトは選択できないようにした(選択しても、またログイン状態になるので意味がない)
+++ trac-0.9.5-ja-1kai/trac/web/auth.py 2006-06-18 01:54:37.000000000 +0900
@@ -64,7 +64,8 @@
def get_navigation_items(self, req):
if req.authname and req.authname != 'anonymous':
yield ('metanav', 'login', '%s としてログイン中' % req.authname)
- yield ('metanav', 'logout',
+ if not self.config.getbool('trac' , 'must_login'):
+ yield ('metanav', 'logout',
Markup('<a href="%s">ログアウト</a>'
% escape(self.env.href.logout())))
else:
--- trac-0.9.5-ja-1/trac/web/standalone.py 2006-01-09 16:48:09.000000000 +0900
+++ trac-0.9.5-ja-1kai/trac/web/standalone.py 2006-06-18 01:16:14.000000000 +0900
@@ -288,7 +288,8 @@
return
req.remote_user = None
- if path_info == '/login':
+ if path_info == '/login' or \
+ env.config.getbool('trac' , 'must_login') :
auth = self.server.auths.get(project_name) or \
self.server.auths.get('*')
if not auth:
- trac.iniの[trac]セクションに設定を追加する。
[trac]
:
must_login = true
「subversionのリポジトリブラウザとBTSとWikiが一緒になった」というtracを使ってみる。というか以前から使っていたのだが環境の問題かあちこちいじらないと動かなかったのでメモ。
当初はmod_pythonを用いようとしたのだが、Segmentation FaultまたはAbortでapacheが死んでしまった。以下の要求を満たす形で構築したい、ので試行錯誤した。
- ブラウザだけ用意できればアクセスできるようにしたい。httpsでアクセスしたい。
- 外から見た場合に、https://xxxx.exmple.com:2222/のようなポート番号指定のURLではなくhttps://xxxx.example.com/project/のように表現したい。→穴を開けるポートを増やしたくない。
- ウチではサーバにlinuxを使っている。外部公開しているポイントの穴を増やすことも避けたいが、iptablesによる許可ポートもできるだけ増やしたくない。
- 認証はapacheの機能で行いたい。
- これは「セキュリティはインフラサイドで確保するもの」という自身のポリシーもあるけど、Active DirectoryでSubversionのリポジトリとtracの認証を共通化したかった+apacheの認証モジュールの多さと設定の柔軟さを活かしたいから、という理由
- 外部からはhttpsで接続したい
- プライベートなプロジェクトを管理したいから。
- CGI型はかなりレスポンスが遅くなるので採用しない。
- なのでmod_pythonを選択したいところだが、apacheが死んでしまう問題を解決できなかったのでスタンドアロン型を選んだ。
- また、スタンドアロン型といっても管理するアクセスログが増えるのはイヤで、apacheのログとひっくるめて管理できるようにしたい。
- フロントエンドはapacheで、認証もここで行う。ついでにSSLも可に。
- 内部でtracdを動かし、apache→tracd間はmod_proxyでリバースプロクシでつなぐ
導入
- trac本家はここ(http://www.edgewall.com/trac/)だが、日本語化したものをインタアクト株式会社が公開しているのでありがたく使わせてもらう。
- ClearSilver、SQLite、パイソンバインディングなどもろもろ用意せねばならんのだが、このあたりは既に詳しいページが多数あるので割愛。
設定
- SSLの設定はあちこちに情報があるので割愛。
- tracの設定もあちこちに情報があるので割愛。ただ、今回想定している環境は「外向きにはhttps」なので、デフォルトのままだとtracdが返す情報(特にLocationヘッダ)でhttpスキームを使われてしまうのでbase_urlを指定した。
(projectenv)/conf/trac.ini
[trac]
base_url = https://外向け/trac
- リバースプロクシは以下のような感じ。(Location内にリバースプロクシ設定を書いているので、パラメータが略されている点に注意)
<Location /trac>
ProxyPass http://tracdのIP:tracdのポート/trac
ProxyPassReverse http://tracdのIP:tracdのポート/trac
</Location> - 認証の設定もあちこちに情報があるので割愛。ふつーにtracのURL範囲を認証要に設定すればよい。BasicでもKerberosでも任意選択できる。
- ここまででhttps://外向け/trac/へのアクセスがapacheにより認証されたうえで、内部で動作しているtracdにプロクシされるはず。ウチの環境の場合、内部は内部ネットワーク向けのDNSを動かしていて、外向けのFQDNと内部ホストのFQDNを同じにしているので(IPは異なる)、これが異なる場合はcookie関係の設定が必要かも。
tracユーザの識別問題
- trac上のユーザ識別も必要だ。なぜならチケットの発行やマイルストーンの編集などの権限判断は「trac上のユーザ」で行われるから。したがってapacheで認証したユーザをなんとかしてtracdにも認識させたい。tracdで使う認証がBasicまたはDigestであれば、apacheとtracdの間で認証データベースを共有することで解決できる。しかし今回のように「BasicでもDigestでもなくKerberosが使いたい」「mod_proxyを介している」という条件が加わると、tracdの認証オプションだけは解決できないようだ。
- で、「apacheで認証したリクエストだけがtracdに到達する」という状況から「tracdへのアクセスは認証済み」と考える。Authorizationフィールドからユーザ名を抜き出してtracユーザとして使用することにする。
- 下のようなパッチをあてて、tracdのコマンドラインオプションと「認証しない認証クラス」を捏造する。
--- trac-0.9.5-ja-1/scripts/tracd 2005-12-02 10:10:17.000000000 +0900
+++ trac-0.9.5-ja-1kai/scripts/tracd 2006-06-04 20:38:59.000000000 +0900
@@ -16,6 +16,7 @@
# Author: Jonas Borgstr
from trac.web.standalone import BasicAuth, DigestAuth, TracHTTPServer
+from trac.web.TrustedAuth import *
import getopt
import locale
@@ -56,6 +57,7 @@
try:
opts, args = getopt.getopt(sys.argv[1:], "a:p:b:de:",
["auth=", "port=", "hostname=","daemonize",
+ "trusted-auth=",
"env-parent-dir=", "basic-auth="])
except getopt.GetoptError, e:
print e
@@ -66,6 +68,8 @@
add_auth(auths, a, DigestAuth)
if o == '--basic-auth':
add_auth(auths, a, BasicAuth)
+ if o == '--trusted-auth':
+ add_auth(auths, a, TrustedAuth)
if o in ("-p", "--port"):
port = int(a)
elif o in ("-b", "--hostname"):
--- trac-0.9.5-ja-1/trac/web/TrustedAuth.py 1970-01-01 09:00:00.000000000 +0900
+++ trac-0.9.5-ja-1kai/trac/web/TrustedAuth.py 2006-06-04 20:40:22.000000000 +0900
@@ -0,0 +1,67 @@
+# -*- coding: utf-8 -*-
+#
+# This is adhoc auth?-module under tracd.
+#
+# If you are using tracd and external proxy(ex. mod_proxy) with some auth
+# method, the access for tracd is already authorized.
+#
+# TrustedAuth regard the user as already trusted.
+# The user extraced from Authorization field.
+#
+# Author: tckz<tckz@nifty.com>
+#
+
+from trac import util, __version__
+from trac.web.api import Request
+from trac.web.cgi_frontend import TracFieldStorage
+
+import urllib2
+
+try:
+ from base64 import b64decode
+except ImportError:
+ from base64 import decodestring as b64decode
+
+
+class TrustedAuth:
+ def __init__(self, dummy, realm):
+ self.realm = realm
+
+ def send_auth_request(self, req):
+ req.send_response(401)
+ req.end_headers()
+
+ def parse_auth_header(self, authorization):
+ values = {}
+ for value in urllib2.parse_http_list(authorization):
+ n, v = value.split('=', 1)
+ if v[0] == '"' and v[-1] == '"':
+ values[n] = v[1:-1]
+ else:
+ values[n] = v
+ return values
+
+ def do_auth(self, req):
+ if not 'Authorization' in req.headers:
+ self.send_auth_request(req)
+ return None
+
+ user = ""
+ if req.headers['Authorization'].startswith('Basic'):
+ auth = req.headers['Authorization'][len('Basic')+1:]
+ auth = b64decode(auth).split(':')
+ if len(auth) == 2:
+ user, password = auth
+ elif req.headers['Authorization'].startswith('Digest'):
+ auth = self.parse_auth_header(req.headers['Authorization'][7:])
+ if auth.has_key('username'):
+ user = auth['username']
+ else:
+ self.send_auth_request(req)
+
+ if user == "":
+ self.send_auth_request(req)
+ return None
+
+ return user
+
- tracd起動時のコマンドラインは↓のような感じで。--trusted-authの引数にカンマ区切りの3要素を指定しなければならないのは、元々tracdのadd_authメソッドに手を入れていないから。「*」部分はtracプロジェクト名。「*」にすると全部のプロジェクトが対象となる(元々の仕様)
/usr/bin/tracd --trusted-auth '*,,' -p ポート -b localhostとか -d -e parent-dir-of-envs
