2011年4月17日日曜日

Pythonの時刻処理(時計、タイムゾーン変換)

Pythonでの時間の扱いがわけわかんないので、少しまとめておく
(Python 2.6で動作確認)

標準モジュール

o time

低レベルの時刻処理。Cの関数呼び出し。
時刻はタイムスタンプ(整数型、time_t)、または構造体(struct_time)で表現。

o datetime

高レベルの時刻処理。
オブジェクトとして時刻を扱える。

o calendar

カレンダーを扱うためのライブラリ。

拡張モジュール

拡張パッケージをいくつか導入(pip install)しておくと便利。

o pytz

タイムゾーンの一覧。

o python-dateutil

日付関連の便利なユーティリティ。

タイムゾーンの扱い

アプリケーション内での時刻処理には、datetimeオブジェクトを使う。datetimeは、明示的に指定しない限りは「タイムゾーンなし」になる。このオブジェクトが示すのは「システムの時計」であり、地球上のどこかの「現地時間」を表してはいない。(Pythonでは、前者を"native object"、後者を"aware object"と呼ぶ)

>>> datetime.datetime.now()
datetime.datetime(2011, 4, 17, 19, 15, 50, 533798)

タイムゾーンを明示的に指定したければ、pytzを利用する。

>>> datetime.datetime.now(tz=pytz.timezone('Asia/Tokyo'))
datetime.datetime(2011, 4, 17, 19, 15, 52, 863696, tzinfo=<DstTzInfo 'Asia/Tokyo' JST+9:00:00 STD>)
または
>>> pytz.timezone('Asia/Tokyo').localize(datetime.datetime.now())
datetime.datetime(2011, 4, 17, 19, 15, 52, 863696, tzinfo=<DstTzInfo 'Asia/Tokyo' JST+9:00:00 STD>)

時刻表現の標準化

タイムゾーンの変換を考える前に、時刻の表現を標準化したい。

まず、時刻はすべてUTCで持つことにする。タイムスタンプ(整数)で現在時刻を得るには次のようにする。

>>> int(time.time())
1303036188

datetimeで現在時刻を得るには次のようにする。

>>> datetime.datetime.utcnow()
datetime.datetime(2011, 4, 17, 10, 30, 50, 193172)

タイムスタンプとdatetimeの相互変換は次のようにする。

>>> dt = datetime.datetime.utcnow()
>>> calendar.timegm(dt.timetuple())
1303036386
>>> datetime.datetime.utcfromtimestamp(1303036386)
datetime.datetime(2011, 4, 17, 10, 33, 6)

datetimeはバイト表現に向いていないので、ファイル等に格納するときは64ビット整数のタイムスタンプとし、アプリケーションでdatetimeに変換する。ただし、DBに格納するときはdatetimeのままO/Rマッパーに渡した方が扱いやすい。

タイムゾーンの変換

上記のように標準化された時刻は、暗黙的にUTCでの時刻を示しているが、オブジェクトとしてはタイムゾーンの情報を持っていない。タイムゾーンを扱うときには、それがUTCであることを明示的に示してやる。

>>> pytz.utc.localize(dt)
datetime.datetime(2011, 4, 17, 10, 33, 6, 575205, tzinfo=<UTC>)

その上で、目的のタイムゾーンへと変換を行う。

>>> pytz.utc.localize(dt).astimezone(pytz.timezone('Asia/Tokyo'))
datetime.datetime(2011, 4, 17, 19, 33, 6, 575205, tzinfo=<DstTzInfo 'Asia/Tokyo' JST+9:00:00 STD>)

一連の処理を簡単にするためにクラス化を行う。例えば、次のようなクラスを考える。

class User(object):
    def __init__(self):
        self.timezone = 'Asia/Tokyo'

標準化された時刻dtを、オブジェクトのタイムゾーンに変換するためのメソッドをlocalizeとする。

    def localize(self, dt):
        return pytz.utc.localize(dt).astimezone(pytz.timezone(self.timezone))

同様に、標準化されたタイムスタンプを、オブジェクトのタイムゾーンに変換するためのメソッドをlocaltimeとする。

    def localtime(self, timestamp):
        return self.localize(datetime.datetime.utcfromtimestamp(timestamp))

時刻の表示

ローカル時間へと変換された時刻は、通常のdatetimeオブジェクトとしてフォーマットできる。

>>> u = User()
>>> t = int(time.time())
>>> print u.localtime(t)
2011-04-17 20:26:14+09:00
>>> print u.localtime(t).ctime()
Sun Apr 17 20:26:14 2011
>>> print u.localtime(t).isoformat()
2011-04-17T20:26:14+09:00
>>> print u.localtime(t).strftime('%Y-%m-%d %H:%M:%S %Z')
2011-04-17 20:26:14 JST

TODO: ロケールを指定した時刻の出力について。

時刻の読み込み

フォーマットされた時刻を読み取るにはstrptimeが使える。

>>> datetime.datetime.strptime('2011-04-18 06:02:32 GMT', '%Y-%m-%d %H:%M:%S %Z')
datetime.datetime(2011, 4, 18, 6, 2, 32)

ただ、文字列に含まれるタイムゾーンは、うまく読み取ってくれないみたい。あらかじめ"GMT"のようにわかっているならそれを前提としてもいいが、そうでなければ自前で処理する必要がありそう。(探せばライブラリがあるかも)

python-dateutilという外部モジュールを使うと、明示的にフォーマットを指定せずとも、ライブラリ側でよきにはからってくれるみたい。(タイムゾーンもある程度みてくれるようだ)

>>> dateutil.parser.parse('Sun, 17 Apr 2011 21:32:07 JST')
datetime.datetime(2011, 4, 17, 21, 32, 7, tzinfo=tzlocal())

タイムゾーン付きで読んだ時刻は、次のようにして標準化する。

>>> dt = dateutil.parser.parse('Sun, 17 Apr 2011 21:32:07 JST')
>>> dt.astimezone(pytz.utc).replace(tzinfo=None)
datetime.datetime(2011, 4, 17, 12, 32, 7)
>>> calendar.timegm(dt.astimezone(pytz.utc).timetuple())
1303043527

日付の計算

将来の時間を計算するときは、datetime.timedeltaを用いる。

>>> dt = datetime.datetime.utcnow()
>>> dt
datetime.datetime(2011, 4, 17, 12, 36, 18, 792871)
>>> dt + datetime.timedelta(seconds=60)
datetime.datetime(2011, 4, 17, 12, 37, 18, 792871)
>>> dt + datetime.timedelta(days=30)
datetime.datetime(2011, 5, 17, 12, 36, 18, 792871)

ただ、datetime.timedeltaは秒単位、日単位の指定しか出来ないので、「半年後」などを指定するのは難しい。そのようなときは、dateutil.relativedelta.relativedeltaが使える。

>>> dt + dateutil.relativedelta.relativedelta(months=6)
datetime.datetime(2011, 10, 17, 12, 36, 18, 792871)

時間の差分

「〇〇分前」とか「残り○時間○分」とか表示したいときは、Djangoであればdjango.utils.timesinceが使える。

>>> dt = datetime.datetime.now()
>>> django.utils.timesince.timesince(dt - datetime.timedelta(seconds=300))
u'5 minutes'
>>> django.utils.timesince.timeuntil(dt + datetime.timedelta(seconds=300))
u'4 minutes'

これらは標準のフィルタとして次のように使える。

{{ blog_date|timesince:comment_date }}

TODO: タイムゾーンを考慮した記述。

その他

datetime.datetime.utcnow()を使うとマイクロ秒まで返されてしまう。秒単位の方がよければ次のようにする。

>>> datetime.datetime.utcnow().replace(microsecond=0)
datetime.datetime(2011, 4, 17, 13, 6, 35)


2010年7月23日金曜日

仮想ルータでゲストネットワークを構築する

image

社内のネットワークとは別に、ゲスト用にネットワーク回線を提供したいことがある。

このようなネットワークでは、インターネットとの通信は許しつつも、社内のPCや各種サーバへのアクセスは防ぎたい。

そのためには、ファイアーウォールの機能を備えたルータを置くことが必要だけれども、これを仮想化することを考えたい。(*1)

仮想化のシステムにはVMware ESXi、ソフトウェアルータとしてはvyattaを利用する。

(*1) 最近の無線ルータにはもともとゲストネットワークの機能が入っているものがあり、そうした機器では自前でルータを立てる必要はない。

物理的な接続

ゲスト用に物理的なHUBを用意する。無線LAN(Wi-Fi)も提供するならば、アクセスポイントをそこにつなげる。

前回の記事と同様に、ゲストネットワーク(Guest Network)にVLAN IDを一つ割り当てて、L2スイッチを通して仮想化ホストとの接続を行う。

image

仮想マシンを用意する

ESXiのネットワーク構成管理で、Guest Networkを作成して、VLAN IDを割り当てる。

メニューからvyattaをデプロイする。今回は単純にネットワークカードを2枚にした。eth0は社内ネットワークに接続し、eth1はGuest Networkに接続する。

vyatta

ルータの基本設定

ホスト名、タイムゾーン、NTPサーバなんかを設定。

vyatta@rt:~$ configure
[edit]
vyatta@rt# set system host-name rt.guest
[edit]
vyatta@rt# set system time-zone Asia/Tokyo
[edit]
vyatta@rt# set system ntp-server ntp.nict.jp
[edit]

ネームサーバは社内ネットワークのを使ってもいいんだが、社内サーバの名前解決が出来ても嬉しくないので、外部のもの(プロバイダのDNSなど)を利用。ここではGoogle Public DNSを設定。

vyatta@rt# set system name-server 8.8.8.8
[edit]
vyatta@rt# set system name-server 8.8.4.4
[edit]

ネットワーク設定

eth0のアドレスはDHCPで取得し、eth1には固定で192.168.1.1/24を割り当てる。

vyatta@rt# set interfaces ethernet eth0 address dhcp
[edit]
vyatta@rt# set interfaces ethernet eth1 address 192.168.1.1/24
[edit]

eth0に向かうパケットはIPマスカレードする。

vyatta@rt# set service nat rule 10 outbound-interface eth0
[edit]
vyatta@rt# set service nat rule 10 type masquerade
[edit]

ファイアーウォール設定

社内ネットワークへのアクセスは拒否するようファイアーウォールを設定する。これは次のルールによって行う。

  1. 192.168.0.0/16に向けたパケットは拒否する。
  2. 172.16.0.0/12に向けたパケットは拒否する。
  3. 10.0.0.0/8に向けたパケットは拒否する。
  4. それ以外のパケットは許可する。

まずはこれをルールFW-1として作成。

vyatta@rt# set firewall name FW-1 rule 1 destination address 192.168.0.0/16
[edit]
vyatta@rt# set firewall name FW-1 rule 1 action reject
[edit]
vyatta@rt# set firewall name FW-1 rule 2 destination address 172.16.0.0/12
[edit]
vyatta@rt# set firewall name FW-1 rule 2 action reject
[edit]
vyatta@rt# set firewall name FW-1 rule 3 destination address 10.0.0.0/8
[edit]
vyatta@rt# set firewall name FW-1 rule 3 action reject
[edit]
vyatta@rt# set firewall name FW-1 rule 4 action accept
[edit]

このルールをeth1から入ってくるパケットに対して適用する。

vyatta@rt# set interface ethernet eth1 firewall in name FW-1
[edit]

逆方向の通信、つまり社内ネットワークからゲストネットワークに向けた通信も防いだ方がいいのだろうけど、どうせFW-1によって双方向の通信は拒否されるので、上記設定で十分だろう。

サービス設定

必要に応じて、ゲストネットワークのためのDNSやDHCPサーバを動かす。

eth1からのDNS問い合わせは、最初に設定した外部のDNSサーバに転送する。

vyatta@rt# set service dns forwarding listen-on eth1
[edit]
vyatta@rt# set service dns forwarding system
[edit]

DHCPで配るアドレスは192.168.1.100~199とし、デフォルトルータやDNSのアドレスは自分自身にする。

vyatta@rt# set service dhcp-server shared-network-name ETH1_POOL \
  subnet 192.168.1.0/24 start 192.168.1.100 stop 192.168.1.199
[edit]
vyatta@rt# set service dhcp-server shared-network-name ETH1_POOL \
  subnet 192.168.1.0/24 default-router 192.168.1.1
[edit]
vyatta@rt# set service dhcp-server shared-network-name ETH1_POOL \
  subnet 192.168.1.0/24 dns-server 192.168.1.1
[edit]

設定の反映と保存

ここまでの設定をシステムに反映(commit)させる。問題なさそうなら保存(save)して完了。

vyatta@rt# commit
...
[edit]
vyatta@rt# save
Saving configuration to '/opt/vyatta/etc/config/config.boot'...
Done
[edit]
vyatta@rt# exit
exit

2010年7月15日木曜日

小規模オフィスのネットワーク仮想化を考える

社内のサーバがどんどん仮想化されていく今日この頃。ネットワークが少し複雑になると、仮想環境と物理環境をどう接続するかで悩まされる。まだまだ変更するかもだけど、とりあえず現時点でのパターンをまとめてみたい。

基本的な構成として、大きく三種類のネットワークを考える。

image

1. グローバルネットワーク(Global Network)

固定IPを持つサーバ群が接続される。いわゆるDMZ。

2. 管理用ネットワーク(Management Network)

ネットワーク機器や仮想化システム、管理者のPCなどが接続される。DMZのサーバも、メンテナンス用につなぐといい。

3. 社内ネットワーク(Office Network)

社内のLAN。完全にフラットなこともあるし、いくつかのセグメントに分割されることもある。

インターネットへの経路

image

DMZの設定は人それぞれだろうけど、うちの場合はブロードバンドルータをUnnumberdに構成して、DMZの各マシンにグローバルのIPアドレスを分配してる。

それ以外のマシンは、NAT(IPマスカレード)でアドレス変換してインターネットに出ていく。ブロードバンドルータがNATしてくれるなら簡単なんだけど、UnnumberdとNATがうまく両立できなかったので、社内用のルータ(後述)でNATすることにした。

ネットワークに名前を付ける

すべてのネットワークセグメントに名前を付けて、VLAN IDとネットワークアドレスを割り当てる。ここでは次の三つを考える。

VLAN 1 Management Network 192.168.0.0/24
VLAN 2 Global Network x.x.x.x/xx
VLAN 3 Office Network 192.168.1.0/24

L2スイッチを接続する

物理的なネットワークを仮想化された世界へといざなうためにL2スイッチを導入する。

image

L2スイッチの役割は、各ネットワークセグメントに個別のVLAN IDを割り当てて、パケットにVLANタグを付けて上流に受け渡すことである。これを実現するには、L2スイッチのVLAN設定を、例えば次のように構成する。(表の見方についてはこちら

VLAN P1 P2 P3 P4… Network
1 U       Management
2 T U     Global
3 T   U   Office
PVID 1 2 3    

うちではProCurve Switch 1800-8Gを使ってて、次のように設定してある。(VLAN 5まで利用)

vlan

これでポート1にはVLANタグの付いたパケットが流れるので、これを仮想マシンから利用できるようにする。

仮想化ホストを接続する

ポート1を仮想化ホストに接続する。複数のホスト機がある場合、間にハブを入れて分配すると楽。

image

仮想化ホストとしてはVMware ESXiを使っている。vSwitchにネットワークを必要なだけ登録して、それぞれにVLAN IDを割り当ててやる。VLAN 1(Management Network)についてはタグなしで届くので、VLANの設定は行わない。

esxi

ここで“Routing Network”というのは特別なネットワークで、VLAN IDを4095に設定する。こうするとすべてのVLANのパケットがタグのついたまま届くようになる。これは後述するように、ルータを仮想化するときに利用できる。

以上の設定を正しく行えば、仮想マシンを各ネットワークに接続することにより、任意の物理的なネットワークとつながるようになる。

ルータを仮想化する

ネットワークセグメントが複数あると、それらの相互接続のためにルータが必要となる。ここでは、すべてのネットワークにVLAN IDを割り当てているので、VLAN間ルーティングが出来るといい。

ルータの仮想化にはvyattaを利用している。VMware用に仮想アプライアンスが提供されていて、コマンド一つで導入できるようになっている。

vyatta

仮想アプライアンスを、先ほどのRouting Networkに接続する。VLAN間ルーティングを行うには、ルータのIPアドレス設定に”interfaces ethernet ethX vif”コマンドが使える。例えば、次のようにアドレスを設定する。

interfaces {
  ethernet eth0 {
    address 192.168.0.1/24     # VLAN 1
    hw-id 00:0c:29:00:00:00
    vif 2 {
      address x.x.x.x/xx           # VLAN 2
    }
    vif 3 {
      address 192.168.1.1/24   # VLAN 3
    }
  }
}

必要に応じて、DNSやDHCP、IPマスカレードやファイアーウォールの設定を行う。IPマスカレードであれば、次のような設定になる。

service {
  nat {
    rule 10 {
      outbound-interface eth0.2
      type masquerade
    }
  }
}

まとめ

最終的に、物理的なネットワークは次のようになる。

image

ひとつ問題点として、ルータを仮想化してしまうと、インターネットとの通信がすべて仮想化ホストを通ることになり、システムに負荷が掛かる。また、仮想マシンを落とすたびにインターネットに繋がらなくなるので、けっこう不便。インターネットとの接続には物理的なルータを通したほうが何かと楽ではある。

あと、セグメントの数が3つやそこらであれば、苦労してVLANを活用するよりも、仮想化ホストに物理的にLANカードを増設してやるほうが楽だったりする。VLANを使うと自由度は高まるけど、慣れないうちは苦労する。

はたして苦労するだけの価値があるのかどうかよくわからないが、いろいろ試した末にこういう形に落ち着いたので、簡単ながらまとめてみた。