CentOS 5にRedmineを導入する
下準備
rubyとsqlite3をインストール
$ yum install ruby $ yum install sqlite
RubyGemsをインストールするためにyumを設定
/etc/yum.repos.d/dlutter.repo を作成する。ここでは、dlutterさんの作ったリポジトリを拝借する。内容は以下の通り。
[dlutter] name=Unsupported RHEL5 packages (lutter) baseurl=http://people.redhat.com/dlutter/yum/rhel/5/$basearch/ enabled=0 gpgcheck=0
RubyGemsをインストール
$ yum --enablerepo=dlutter -y install rubygems.noarch
Ruby on Railsをインストール
$ gem install rails -v 2.1.2
この処理中、
Bulk updating Gem source index for: http://gems.rubyforge.org
と表示されて、処理が進まなくなることがある。これは、メモリ不足の可能性がある。VMware上で、256MBのマシンだと発生したが、512MBまであげてみたところ、この問題は解決した。
また、
Could not find rails (> 0) in any repository
と表示された場合には、
$ gem install --remote rails --include-dependencies
を試してみる。
注意点として、Rails 2.1.2じゃないと、Redmine 0.8 安定版との組み合わせだといろいろとエラーまみれになったので、ここでは、バージョンを明示的に指定している。ちなみに、すでにインストールされているRailsのバージョンを知るには、次のようにする。
$ gem list rails *** LOCAL GEMS *** rails (2.1.2) Web-application framework with template engine, control-flow layer, and ORM.
sqlite3-rubyをインストール
sqlite3-rubyをインストールするに当たっては、ruby-devel、sqlite-develが必要になるので、あらかじめ、インストールしておく。
$ yum install ruby-devel $ yum install sqlite-devel
その後にgemをつかってインストールする。
$ gem insall sqlite3-ruby
Redmineのインストール
ここまできてやっとRedmineをインストールできる。ここまででも、情報が分散しているとなかなか大変。
Redmineのチェックアウト
ここでチェックアウトするディレクトリは、ウェブに公開するディレクトリではないところの方がいい。とりあえず、/var/wwwあたりにチェックアウトしておけばいいのではないかな。それについてはまた後ほど。
$ cd /var/www $ svn co http://redmine.rubyforge.org/svn/branches/0.8-stable redmine
今回は、保守的に、0.8 安定版を使う。というか、最新版だと、全く動かせなかったのが理由。ハマリまくってもいい人以外は安定版をおすすめ。
前のところで、あえて、Rails 2.1.2以外をインストールした場合には、redmine/config/environment.rbにあるRAILS_GEM_VERSIONを修正する。しかしながら、手元では、Redmin 0.8 安定版を2.2.2などで動かすことはできなかった。
データベースの設定
redmine/config/database.ymlを作成する。今回は、sqliteを使用しているので、次の設定は、sqliteのデータベースのパスを、redmine/db/redmine.dbに設定する。
production: adapter: sqlite3 dbfile: db/redmine.db timeout: 5000
データベース初期化
redmine/の下で次の作業を行う。
$ rake db:migrate RAILS_ENV=production
次に、デフォルトデータをロードする。ここでは、言語を聞かれるので、必要に応じて、enあるいは、jaを設定する。
$ rake redmine:load_default_data RAILS_ENV=production (in /home/www/html/redmine) Select language: bg, ca, cs, da, de, en, es, fi, fr, gl, he, hu, it, ja, ko, lt, nl, no, pl, pt, pt-br, ro, ru, sk, sr, sv, th, tr, uk, vn, zh, zh-tw [en] ==================================== Default configuration data loaded.
データベース関連のファイルとして、
db/redmine.db db/schema.rb
が作成されるので、バックアップ時には、これらをバックアップする。
また、実は、今回、僕の場合には、bugzillaからの移行だったのだが、それについては、下で説明する。
メール送信用の設定
redmine/config/email.ymlを作成する。たぶん、ほとんどの場合、社内に認証なしで利用できるSMTPサーバーがあるだろうから、それを利用する。認証が必要な場合には、authentication/user_name/passwordを設定する必要がある。
production: delivery_method: smtp smtp_settings: address: smtp.example.com port: 25 domain: example.com authentication: :login user_name: password:
実験的な起動
コマンドラインで次のようにして起動すると、3000/tcpで待ち受け状態となる。初期状態では、admin/adminというユーザーがいるので、そのユーザーでログインして、設定を行う。
$ script/server -e production
この実験的な起動は、くれぐれもファイヤーウォールの内側でやるように。
ある意味では、社内で利用するぐらいなら、ここで設定を終了しても良い。あとは、適当にサービス化すればいいだけ。
Apacheでの起動
とはいえ、うちはApacheじゃないととか、HTTPSでセキュリティを確保したいとか、あるいは、特殊な認証を使わないと行けないとかなると、Apacheの方が断然、カスタマイズがしやすかったりするので、Apacheでの設定方法も書いておきます。
Apacheで起動するには、Passengerというモジュールをインストールする必要があります。
途中、fastthreadをインストールするための質問をされるが基本的には、すべて[Y]で良い。
$ gem install passenger
次に、passenger-install-apache2-moduleを実行するが、gcc-c++, httpd-devel, apr-develなどが必要になるので、適宜、インストールする。
$ passenger-install-apache2-module
このコマンドの完了時に、apacheの設定についてのコメントが表示されるので、それに従って、apacheの設定を修正する。通常は、これを、/etc/httpd/conf.d/redmine.confなどに書くことになる。
LoadModule passenger_module /usr/lib/ruby/gems/1.8/gems/passenger-2.0.6/ext/apache2/mod_passenger.so PassengerRoot /usr/lib/ruby/gems/1.8/gems/passenger-2.0.6 PassengerRuby /usr/bin/ruby
VirtualHostを使ってうんぬんの設定に関しては、結構、世にあふれているが、普通は、Virtualhostを使うなんて贅沢なことはできないので、サブディレクトリ、つまり、
http://www.example.com/redmine/
などの形態で、運用するための設定を行う。このためには、まず、DocumentRootの直下に、さっき、redmineをチェックアウトしたディレクトリ内のpublicディレクトリに対するシンボリックリンクをredmineという名前で張る。
たとえば、さっき、チェックアウトしたディレクトリが、/var/www/redmineとかで、DocumentRootが、/var/www/htmlとかなら、
$ cd /var/www/html $ ln -s /var/www/redmine/public redmine
のような感じになる。注意としては、/var/www/redmineはApacheのユーザー、つまり、apacheで読み書き可能になっていること。
$ chown -R apache.apache /var/www/redmine
とかやれば完璧。
次に、先ほどの、/etc/httpd/conf.d/redmine.confに次のようなものを追加する。
# VirtualHostを利用しないので、自動認識をOFF RailsAutoDetect off # http://www.example.com/redmine/で運用する RailsBaseURI /redmine <Location /redmine> # redmineに対する設定 ... </Location>
これら設定が完了したら、Apacheを再起動する。
/etc/init.d/httpd restart
たぶん、これで完了。
Bugzillaからの移行
これについては、Bugzillaからの移行のスクリプトを作ってくれている人がいるんだけど、古いバージョンのBugzillaに対するスクリプトだったり、ユーザー名に特殊な文字を使っていたりする(どうも、カンマとか丸括弧がダメっぽい)と、移行時にエラーが発生することになる。その場合、ユーザーの名字とか、姓だけを後で修正すればいいので、とりあえず、エラーなしでインポートできるようにがんばらないと行けない。
さらに、元々のスクリプトだと、Issue IDが、SQLのクエリの順番に付与されてしまい、元々のBugzillaのBug IDと一致しなくなってしまう可能性がある。というか、僕の場合、その問題が起きてしまった。
ということで、下記のスクリプトは、その辺に対する場当たり的な修正が入っている。ただし、rubyネイティブではない僕の修正なので注意。
# redMine - project management software # Copyright (C) 2006-2007 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # # Bugzilla migration by Arjen Roodselaar, Lindix bv edited by Oliver Sigge # # Successfully tested with Bugzilla 2.20, Redmine devBuild, Rails 2.1 # # Please note that the users in the Bugzilla database must have a forename and surename desc 'Bugzilla migration script' require 'active_record' require 'iconv' require 'pp' namespace :redmine do task :migrate_from_bugzilla => :environment do module BugzillaMigrate DEFAULT_STATUS = IssueStatus.default CLOSED_STATUS = IssueStatus.find :first, :conditions => { :is_closed => true } assigned_status = IssueStatus.find_by_position(2) resolved_status = IssueStatus.find_by_position(3) feedback_status = IssueStatus.find_by_position(4) STATUS_MAPPING = { "UNCONFIRMED" => DEFAULT_STATUS, "NEW" => DEFAULT_STATUS, "VERIFIED" => DEFAULT_STATUS, "ASSIGNED" => assigned_status, "REOPENED" => assigned_status, "RESOLVED" => resolved_status, "CLOSED" => CLOSED_STATUS } # actually close resolved issues resolved_status.is_closed = true resolved_status.save priorities = Enumeration.get_values('IPRI') PRIORITY_MAPPING = { "P1" => priorities[1], # low "P2" => priorities[2], # normal "P3" => priorities[3], # high "P4" => priorities[4], # urgent "P5" => priorities[5] # immediate } DEFAULT_PRIORITY = PRIORITY_MAPPING["P2"] TRACKER_BUG = Tracker.find_by_position(1) TRACKER_FEATURE = Tracker.find_by_position(2) reporter_role = Role.find_by_position(5) developer_role = Role.find_by_position(4) manager_role = Role.find_by_position(3) DEFAULT_ROLE = reporter_role CUSTOM_FIELD_TYPE_MAPPING = { 0 => 'string', # String 1 => 'int', # Numeric 2 => 'int', # Float 3 => 'list', # Enumeration 4 => 'string', # Email 5 => 'bool', # Checkbox 6 => 'list', # List 7 => 'list', # Multiselection list 8 => 'date', # Date } RELATION_TYPE_MAPPING = { 0 => IssueRelation::TYPE_DUPLICATES, # duplicate of 1 => IssueRelation::TYPE_RELATES, # related to 2 => IssueRelation::TYPE_RELATES, # parent of 3 => IssueRelation::TYPE_RELATES, # child of 4 => IssueRelation::TYPE_DUPLICATES # has duplicate } class BugzillaProfile < ActiveRecord::Base set_table_name :profiles set_primary_key :userid has_and_belongs_to_many :groups, :class_name => "BugzillaGroup", :join_table => :user_group_map, :foreign_key => :user_id, :association_foreign_key => :group_id def login login_name[0..29].gsub(/[^a-zA-Z0-9_\-@\.]/, '') end def email if login_name.match(/^.*@.*$/i) login_name else "#{login_name}@foo.bar" end end def firstname read_attribute(:realname).blank? ? login_name : read_attribute(:realname).split.first[0..29] end def lastname read_attribute(:realname).blank? ? login_name : read_attribute(:realname).split[1..-1].join(' ')[0..29] end end class BugzillaGroup < ActiveRecord::Base set_table_name :groups has_and_belongs_to_many :profiles, :class_name => "BugzillaProfile", :join_table => :user_group_map, :foreign_key => :group_id, :association_foreign_key => :user_id end class BugzillaProduct < ActiveRecord::Base set_table_name :products has_many :components, :class_name => "BugzillaComponent", :foreign_key => :product_id has_many :versions, :class_name => "BugzillaVersion", :foreign_key => :product_id has_many :bugs, :class_name => "BugzillaBug", :foreign_key => :product_id end class BugzillaComponent < ActiveRecord::Base set_table_name :components end class BugzillaVersion < ActiveRecord::Base set_table_name :versions end class BugzillaBug < ActiveRecord::Base set_table_name :bugs set_primary_key :bug_id belongs_to :product, :class_name => "BugzillaProduct", :foreign_key => :product_id has_many :descriptions, :class_name => "BugzillaDescription", :foreign_key => :bug_id end class BugzillaDescription < ActiveRecord::Base set_table_name :longdescs belongs_to :bug, :class_name => "BugzillaBug", :foreign_key => :bug_id def eql(desc) self.bug_when == desc.bug_when end def === desc self.eql(desc) end def self.inheritance_column "inh_type" end def text if self.thetext.blank? return nil else self.thetext end end end def self.establish_connection(params) constants.each do |const| klass = const_get(const) next unless klass.respond_to? 'establish_connection' klass.establish_connection params end end def self.migrate # Profiles print "Migrating profiles\n" $stdout.flush User.delete_all "login <> 'admin'" users_map = {} users_migrated = 0 BugzillaProfile.find(:all).each do |profile| user = User.new user.login = profile.login user.password = "bugzilla" user.firstname = profile.firstname user.lastname = profile.lastname user.mail = profile.email user.status = User::STATUS_LOCKED if !profile.disabledtext.empty? user.admin = true if profile.groups.include?(BugzillaGroup.find_by_name("admin")) if (user.login.nil?) user.login = user.mail end if user.firstname.nil? || user.firstname.blank? user.firstname = "dummy" else user.firstname = user.firstname.scan(/([\x2d-\x7f])/).join end if user.firstname.blank? user.firstname = "blank" end if user.lastname.nil? || user.lastname.blank? user.lastname = "dummy" else user.lastname = user.lastname.scan(/([\x2d-\x7f])/).join end if user.lastname.blank? user.lastname = "blank" end result = user.save user.errors.each{|col,err| puts " - #{col.to_s.humanize} #{err}" } print user.mail+","+user.firstname+","+user.lastname+"\n" next unless result users_migrated += 1 users_map[profile.userid] = user print '.' $stdout.flush end # Products puts print "Migrating products" $stdout.flush Project.destroy_all projects_map = {} versions_map = {} categories_map = {} BugzillaProduct.find(:all).each do |product| project = Project.new project.name = product.name project.description = product.description project.identifier = product.name.downcase.gsub(/[0-9_\s\.]*/, '')[0..15] project.trackers[0] = TRACKER_BUG #puts "Name: #{product.name}; Identifier: #{project.identifier}\n" next unless project.save projects_map[product.id] = project print "." $stdout.flush # Enable issue tracking enabled_module = EnabledModule.new( :project => project, :name => 'issue_tracking' ) enabled_module.save # Components product.components.each do |component| category = IssueCategory.new(:name => component.name[0,30]) category.project = project category.assigned_to = users_map[component.initialowner] category.save categories_map[component.id] = category end # Add default user roles 1.upto(users_map.length) do |i| membership = Member.new( :user => users_map[i], :project => project, :role => DEFAULT_ROLE ) membership.save end end # Bugs puts "\n" print "Migrating bugs" Issue.destroy_all issues_map = {} skipped_bugs = [] BugzillaBug.find(:all).sort_by {|bug| bug.bug_id}.each do |bug| #BugzillaBug.find(:all).each do |bug| issue = Issue.new( :project => projects_map[bug.product_id], :tracker => TRACKER_BUG, :subject => bug.short_desc, :description => bug.descriptions.first.text || bug.short_desc, :author => users_map[bug.reporter], :priority => PRIORITY_MAPPING[bug.priority] || DEFAULT_PRIORITY, :status => STATUS_MAPPING[bug.bug_status] || DEFAULT_STATUS, :start_date => bug.creation_ts, :created_on => bug.creation_ts, :updated_on => bug.delta_ts ) issue.category = categories_map[bug.component_id] unless bug.component_id.blank? issue.assigned_to = users_map[bug.assigned_to] unless bug.assigned_to.blank? if issue.save print '.' else issue.id = bug.bug_id skipped_bugs << issue print "!" next end $stdout.flush # notes bug.descriptions.each do |description| # the first comment is already added to the description field of the bug next if description === bug.descriptions.first journal = Journal.new( :journalized => issue, :user => users_map[description.who], :notes => description.text, :created_on => description.bug_when ) if (journal.user.nil?) journal.user = User.find(:first) end next unless journal.save end end puts puts puts "Profiles: #{users_migrated}/#{BugzillaProfile.count}" puts "Products: #{Project.count}/#{BugzillaProduct.count}" puts "Components: #{IssueCategory.count}/#{BugzillaComponent.count}" puts "Bugs #{Issue.count}/#{BugzillaBug.count}" puts if !skipped_bugs.empty? puts "The following bugs failed to import: " skipped_bugs.each do |issue| print "#{issue.id}, reason: " issue.errors.each{|error| print "#{error}"} puts end end end puts puts "WARNING: Your Redmine data will be deleted during this process." print "Are you sure you want to continue ? [y/N] " break unless STDIN.gets.match(/^y$/i) # Default Bugzilla database settings db_params = {:adapter => 'mysql', :database => 'bugzilla', :host => 'localhost', #:port => 3308, :socket => '/var/lib/mysql/mysql.sock', :username => 'root', :password => 'password', :encoding => 'utf8' } puts puts "Please enter settings for your Bugzilla database" [:adapter, :host, :database, :username, :password].each do |param| print "#{param} [#{db_params[param]}]: " value = STDIN.gets.chomp! db_params[param] = value unless value.blank? end BugzillaMigrate.establish_connection db_params BugzillaMigrate.migrate end end end
このスクリプトを、redmine/lib/task/migrate_from_bugzilla.rakeとして保存し、
rake redmine:migrate_from_bugzilla RAILS_ENV="production"
とやるとインポート作業を行ってくれる。ただし、移行中にエラーが発生したら、データベースを最初の状態に戻してから、再度実行すること。そうしないと、データベースの一部の値が変な状態になる(Issue IDの初期値とか)。
あと、このスクリプトでは、パスワードを移行できないので、すべてのユーザーのパスワードは、デフォルトで、bugzillaになっている。ご注意。