Rails 4をローカルタイムで運用する

いろいろ調べたけど、解決法はあっさり簡単だったのでメモしておく。
前提としては、データベースへの保存は UTC、アプリでの入出力はローカルタイムで行う。

サンプルアプリの作成

簡単なサンプルアプリを作る。

takatoh@nightschool $ rails new sampleapp
takatoh@nightschool $ cd sampleapp
takatoh@nightschool $ rails g scaffold items name:string release:datetime
takatoh@nightschool $ rake db:migrate

サーバを起動して http://localhost:3000/items にアクセスすると下のような画面になる。

sampleapp-1

データを入力してみる

試しに一つ、データを入力してみよう。New Item リンクをクリックして新規データ入力のページに移動。

sampleapp-2

今、日本ではは午前10時51分だ。時刻が UTC で表示されているのがわかる。
データをひとつ入力してみたあとが↓これ。時刻は UTC で表示されている。

sampleapp-3

試しに、コンソールで確認してみよう。

takatoh@nightschool $ rails c
Loading development environment (Rails 4.1.4)
2.1.1 :001 > Item.find(1)
  Item Load (0.1ms)  SELECT  "items".* FROM "items"  WHERE "items"."id" = ? LIMIT 1  [["id", 1]]
 => #<Item id: 1, name: "Sampe item 1", release: "2014-11-22 01:51:00", created_at: "2014-11-22 01:51:30", updated_at: "2014-11-22 01:51:30">

release や created_at、 updated_at の値が 01:51:30 になっている。UTC とは書いていないけど、時刻からデータベースにも UTC で保存されているのがわかる。

アプリの入出力を日本時間にするには

config/application.rb に次の行を追加する。サンプルがコメントアウトされているので、その次辺りに入れればいいだろう。

config.time_zone = 'Asia/Tokyo'

書き加えたら、サーバを起動しなおして、itemsページにアクセスしてみよう。

sampleapp-4

今度は時刻が日本時間(+0900)で表示された!
じゃあ、新しいデータを入力するときはどうか。

sampleapp-5

ここも日本時間になっている。

データベースではどうか

コンソールで確かめてみよう。

Loading development environment (Rails 4.1.4)
2.1.1 :001 > item = Item.find(2)
  Item Load (0.1ms)  SELECT  "items".* FROM "items"  WHERE "items"."id" = ? LIMIT 1  [["id", 2]]
 => #<Item id: 2, name: "SAmple item 2", release: "2014-11-22 02:07:00", created_at: "2014-11-22 02:08:15", updated_at: "2014-11-22 02:08:15"> 
2.1.1 :002 > item
 => #<Item id: 2, name: "SAmple item 2", release: "2014-11-22 02:07:00", created_at: "2014-11-22 02:08:15", updated_at: "2014-11-22 02:08:15">

release や created_at、updated_at の時刻は 02:08:15 になっている。データベースには相変わらず UTC で保存されているのがわかる。
ちなみに、取得した Item クラスのインスタンス item からは日本時間に変換された時刻が返ってくる。

2.1.1 :003 > item.release
 => Sat, 22 Nov 2014 11:07:00 JST +09:00

まとめ

Rails 4 をローカル時間に合わせるには、config/application.rb に1行追記するだけでいい。あとは Rails が全てうまくやってくれる。

Rubyでタイムゾーンの名前から、UTCからのオフセットを表す文字列を得る

タイトル長いな。

Time.new の7つ目の引数には、UTC からのオフセットを表す文字列を指定できる。こんなふうに。

2.1.1 :001 > Time.new(2014, 11, 20, 21, 30, 0, "-06:00")
 => 2014-11-20 21:30:00 -0600

上の、”-06:00″が UTC からのオフセットを表す文字列だ。これをタイムゾーンの名前から得たい、というのが今回の趣旨。また ActiveSupport を使って書いた。

require 'active_support/time'

def utc_offset_str(timezone)
  offset = Time.now.in_time_zone(timezone).utc_offset
  h = offset / (60 * 60)
  m = (offset - h * 60 * 60) / 60
  f = h &gt; 0 ? '+' : '-'

  sprintf("%s%02d:%02d", f, h.abs, m)
end

['Asia/Tokyo', 'America/Chicago'].each do |zone|
  puts zone
  puts utc_offset_str(zone)
end

Time#utc_offset で UTC からのオフセット(秒)が得られるので、それを使って目的の文字列を作っている。実行結果は次の通り。それにしてもベタなコードだなぁ。

takatoh@nightschool $ ruby utc_offset_str.rb
Asia/Tokyo
+09:00
America/Chicago
-06:00

おまけ。タイムゾーン名の一覧は↓ここで発見した。

 cf. タイムゾーンの一覧

さらにおまけ。
ActiveSupport の in_time_zone って ‘Asia/Tokyo’ じゃなくてただの ‘Tokyo’ でも受け付けるくせに ‘JST’ だとエラーになる。

2.1.1 :001 > require 'active_support/time'
 => true 
2.1.1 :002 > Time.now.in_time_zone('Tokyo')
 => Thu, 20 Nov 2014 21:44:24 JST +09:00 
2.1.1 :003 > Time.now.in_time_zone('JST')
ArgumentError: Invalid Timezone: JST
	from /home/takatoh/.rvm/gems/ruby-2.1.1/gems/activesupport-4.1.4/lib/active_support/core_ext/time/zones.rb:71:in `rescue in find_zone!'
	from /home/takatoh/.rvm/gems/ruby-2.1.1/gems/activesupport-4.1.4/lib/active_support/core_ext/time/zones.rb:55:in `find_zone!'
	from /home/takatoh/.rvm/gems/ruby-2.1.1/gems/activesupport-4.1.4/lib/active_support/core_ext/date_and_time/zones.rb:20:in `in_time_zone'
	from (irb):3
	from /home/takatoh/.rvm/rubies/ruby-2.1.1/bin/irb:11:in `<main>'

RubyのTimeオブジェクトをto_sしてTime.parseすると違う時刻になる?

タイトルわかりにくいな。要するにこういうこと。

2.1.1 :001 > require 'time'
 => true 
2.1.1 :002 > now = Time.now
 => 2014-11-19 22:27:36 +0900 
2.1.1 :003 > now2 = Time.parse(now.to_s)
 => 2014-11-19 22:27:36 +0900

Time.now して作ったインスタンス now をいったん文字列に変換して、Time.parse したのが now2。当然同じ時刻を示している。

2.1.1 :004 > now
 => 2014-11-19 22:27:36 +0900 
2.1.1 :005 > now2
 => 2014-11-19 22:27:36 +0900

と思いきや、== で比較してみると false が返ってくる。

2.1.1 :006 > now == now2
 => false

どういうことかと、うーんとなった。

で、ググってみると、同じことしてる人がいるもんだね。

 cf. 「Ruby」Timeクラスの比較 – タナカイチロウの日記

どうやら、Ruby の Time クラスは時刻を浮動小数点で持っていて、上のような操作をしてしまうと、厳密に同じ時刻を再現できないらしい。

2.1.1 :007 > now.to_f
 => 1416403656.075245 
2.1.1 :008 > now2.to_f
 => 1416403656.0

なるほどね〜。こりゃ、ハマりどころだわ。
ちなみに、Time.new で作ったインスタンスだとちゃんと true が返ってくる。

2.1.1 :009 > t = Time.new(2014, 11, 19, 22, 30, 0)
 => 2014-11-19 22:30:00 +0900 
2.1.1 :010 > t2 = Time.parse(t.to_s)
 => 2014-11-19 22:30:00 +0900 
2.1.1 :011 > t
 => 2014-11-19 22:30:00 +0900 
2.1.1 :012 > t2
 => 2014-11-19 22:30:00 +0900 
2.1.1 :013 > t == t2
 => true

任意のタイムゾーンの任意の時刻を得る

タイトルわかりにくいな。要するに、例えばシカゴ時間の2014年11月17日午前7時を表すオブジェクトがほしいってこと。

ActiveSupport の in_time_zone と Time#utc_offset を使って作った。

# encoding: utf-8

require 'active_support/time'

def time_in_zone(timezone, year, month, day, hour = 0, min = 0, sec = 0)
  t = Time.new(year, month, day, hour, min, sec).utc
  now = Time.now
  offset1 = now.utc_offset
  offset2 = now.in_time_zone(timezone).utc_offset
  (t + offset1 - offset2).in_time_zone(timezone)
end

puts time_in_zone('America/Chicago', 2014, 11, 17, 7, 0, 0)
puts time_in_zone('Asia/Tokyo', 2014, 11, 17, 7, 0, 0)
takatoh@nightschool $ ruby time_in_zone.rb
2014-11-17 07:00:00 -0600
2014-11-17 07:00:00 +0900

なんかオフセットを足したり引いたりしてるところがややこしくて時間がかかった。もっとスマートな方法はないもんかな。

11/20追記

Time.new の7つ目の引数に、UTC からのオフセットを表す文字列を与えてやると、同じようなことができる。

2.1.1 :001 > Time.new(2014, 11, 17, 7, 0, 0, "-06:00")
 => 2014-11-17 07:00:00 -0600

問題は、オフセット文字列じゃなくてタイムゾーンの名前から作りたいってことだ。どうやったらいいだろう?

UTCの時刻を任意のタイムゾーンに変換する

ActiveSupport::TimeWithZone を使うのが便利。active_support/time を require する。

2.1.1 :001 > require 'active_support/time'
 => true 

UCT 時刻を America/Chicago 時刻に変換してみる。

2.1.1 :002 > t = Time.now.utc
 => 2014-11-16 11:14:17 UTC 
2.1.1 :003 > tc = t.in_time_zone('America/Chicago')
 => Sun, 16 Nov 2014 05:14:17 CST -06:00 

tc のクラスは Time ではなくて、ActiveSupport::TimeWithZone。

2.1.1 :004 > tc.class
 => ActiveSupport::TimeWithZone

Time と比較もできる。

2.1.1 :005 > t == tc
 => true

TimeZone が違うと時刻表示は当然違うけど、UTC に直すと同じ時刻なので true になる。

Ubuntuにsshで接続する

Windowsマシンから、Ubuntuのマシンにsshで接続できるようにする。
↓このページが参考になった。

 cf. http://www.server-world.info/query?os=Ubuntu_14.04&p=ssh

まずは、ssh サーバを atp-get でインストール。

takatoh@nightschool $ sudo apt-get install openssh-server
[sudo] password for takatoh: 
パッケージリストを読み込んでいます... 完了
依存関係ツリーを作成しています                
状態情報を読み取っています... 完了
以下のパッケージが自動でインストールされましたが、もう必要とされていません:
  firefox-locale-en linux-headers-3.13.0-34 linux-headers-3.13.0-34-generic
  linux-image-3.13.0-34-generic linux-image-extra-3.13.0-34-generic
これを削除するには 'apt-get autoremove' を利用してください。
以下の特別パッケージがインストールされます:
  libck-connector0 openssh-sftp-server ssh-import-id
提案パッケージ:
  rssh molly-guard monkeysphere
以下のパッケージが新たにインストールされます:
  libck-connector0 openssh-server openssh-sftp-server ssh-import-id
アップグレード: 0 個、新規インストール: 4 個、削除: 0 個、保留: 12 個。
374 kB のアーカイブを取得する必要があります。
この操作後に追加で 1,214 kB のディスク容量が消費されます。
続行しますか? [Y/n] Y
取得:1 http://jp.archive.ubuntu.com/ubuntu/ trusty/main libck-connector0 amd64 0.4.5-3.1ubuntu2 [10.5 kB]
取得:2 http://jp.archive.ubuntu.com/ubuntu/ trusty-updates/main openssh-sftp-server amd64 1:6.6p1-2ubuntu2 [34.1 kB]
取得:3 http://jp.archive.ubuntu.com/ubuntu/ trusty-updates/main openssh-server amd64 1:6.6p1-2ubuntu2 [319 kB]
取得:4 http://jp.archive.ubuntu.com/ubuntu/ trusty/main ssh-import-id all 3.21-0ubuntu1 [9,624 B]
374 kB を 0秒 で取得しました (397 kB/s)
パッケージを事前設定しています ...
以前に未選択のパッケージ libck-connector0:amd64 を選択しています。
(データベースを読み込んでいます ... 現在 421617 個のファイルとディレクトリがインストールされています。)
Preparing to unpack .../libck-connector0_0.4.5-3.1ubuntu2_amd64.deb ...
Unpacking libck-connector0:amd64 (0.4.5-3.1ubuntu2) ...
以前に未選択のパッケージ openssh-sftp-server を選択しています。
Preparing to unpack .../openssh-sftp-server_1%3a6.6p1-2ubuntu2_amd64.deb ...
Unpacking openssh-sftp-server (1:6.6p1-2ubuntu2) ...
以前に未選択のパッケージ openssh-server を選択しています。
Preparing to unpack .../openssh-server_1%3a6.6p1-2ubuntu2_amd64.deb ...
Unpacking openssh-server (1:6.6p1-2ubuntu2) ...
以前に未選択のパッケージ ssh-import-id を選択しています。
Preparing to unpack .../ssh-import-id_3.21-0ubuntu1_all.deb ...
Unpacking ssh-import-id (3.21-0ubuntu1) ...
Processing triggers for man-db (2.6.7.1-1ubuntu1) ...
Processing triggers for ureadahead (0.100.0-16) ...
ureadahead will be reprofiled on next reboot
Processing triggers for ufw (0.34~rc-0ubuntu2) ...
libck-connector0:amd64 (0.4.5-3.1ubuntu2) を設定しています ...
openssh-sftp-server (1:6.6p1-2ubuntu2) を設定しています ...
openssh-server (1:6.6p1-2ubuntu2) を設定しています ...
Creating SSH2 RSA key; this may take some time ...
Creating SSH2 DSA key; this may take some time ...
Creating SSH2 ECDSA key; this may take some time ...
Creating SSH2 ED25519 key; this may take some time ...
ssh start/running, process 16328
ssh-import-id (3.21-0ubuntu1) を設定しています ...
Processing triggers for libc-bin (2.19-0ubuntu6.3) ...
Processing triggers for ureadahead (0.100.0-16) ...
Processing triggers for ufw (0.34~rc-0ubuntu2) ...

次に /etc/ssh/sshd_config ファイルを編集。28行目を次のように no にする。

PermitRootLogin no

root でのログインを拒否するわけだね。ちなみにデフォルトでは without-password となっていて、これは鍵認証が必要な設定らしい。

22番ポートを開ける。というか開いていた。

takatoh@nightschool $ sudo ufw status
状態: アクティブ

To                         Action      From
--                         ------      ----
3000/tcp                   ALLOW       Anywhere
9000/tcp                   ALLOW       Anywhere
22/tcp                     ALLOW       Anywhere
8080/tcp                   ALLOW       Anywhere
80                         ALLOW       Anywhere
3000/tcp (v6)              ALLOW       Anywhere (v6)
9000/tcp (v6)              ALLOW       Anywhere (v6)
22/tcp (v6)                ALLOW       Anywhere (v6)
8080/tcp (v6)              ALLOW       Anywhere (v6)
80 (v6)                    ALLOW       Anywhere (v6)

最後に、sshを再起動。

takatoh@nightschool $ initctl restart ssh
initctl:不明なジョブ: ssh

あれ、ダメだ。上の参考ページのマシンとなにか違うのかも。
しょうがないから、/etc/init.d/ssh を直接叩いて再起動。

takatoh@nightschool $ /etc/init.d/ssh restart

これで Ubuntu 側は OK。今度は Windows 側。
接続には Tera Term を使う。

TeraTerm-connect

ホストに IPアドレスを指定して、OKボタンを押す。上に書いたとおり、ポートはデフォルトの22番。

TeraTerm-auth

ユーザー名とパスワードを入力して OK ボタンを押すと、無事接続できた。

TeraTerm-remote