Synapse と Serf でサービスディスカバリ

先週の Immutable Infrastructure Conference #1 [email protected] さんの発表で言及されていた、Synapse と Serf の組み合わせを試してみました。

Synapse とは

Synapse は Airbnb が開発しているサービスディスカバリ用のツールです。簡単に言うと「バックエンドサーバを監視して、HAProxy の設定を書き換えてくれるツール」です。

詳しくは: SmartStack: Service Discovery in the Cloud – Airbnb Engineering

Watcher

Synapse にはバックエンドサーバの増減を監視するレイヤーとして、Watcher が用意されています。
(https://github.com/airbnb/synapse/tree/master/lib/synapse/service_watcher)

現時点では、

  • DnsWatcher (DNS に問い合わせる)
  • DockerWatcher (Docker のコンテナ一覧を取得する)
  • EC2Watcher (EC2 のインスタンス一覧を取得する / 未実装)
  • ZookeeperWatcher (Zookeeper に問い合わせる)

があります。

Airbnb 社では Nerve と ZookeeperWatcher を組み合わせて利用しているようです。

Serf と Synapse

と、ここまでくると、Serf でクラスタリングして、Synapse と連携させたくなってきます。 既存の watcher で Serf と連携するのは難しいので、watcher を実装しました。

Serf にはメンバが追加されたり、削除されたりした際に、event handler を実行する仕組みがあるのでこれを使います。 event handler ではテキストファイルにクラスタのメンバリストを出力しておき、Synapse の watcher 側では inotify などの通知をトリガーに更新します。

Synapse with Serf

Serf の RPC は?

Serf は RPC で操作できるので、watcher で RPC を叩くのがよさそうなのですが、その場合、定期的にメンバリストを取得するしかない(はず)です。 今回はイベントハンドラを利用するために、ファイルベースで実装しました。

FileWatcher

ということで、ファイルを監視する watcher を実装して、プルリクをおくりました。

https://github.com/airbnb/synapse/pull/53

まだ、マージされていないのですが、利用例を紹介します。 (マージされてからブログを書こうかと思っていたのですが、他のプルリクを見る限り、なかなかマージされなさそうな雰囲気なので、先に書いてしまうことにしました)

Synapse + FileWatcher + Serf

Synapse をチェックアウトします。

1
2
$ git clone [email protected]:airbnb/synapse.git
$ cd synapse

本家にはまだ FileWatcher が入っていないのと、(おそらく)バグがあるので、ここでは私のフォークをもってきます。

1
2
3
4
5
$ git remote add ryotarai [email protected]:ryotarai/synapse.git
$ git fetch --all
$ git checkout -b file-watcher-sample
$ git merge ryotarai/file-watcher
$ git merge ryotarai/shared_frontend-is-optional

Vagrant で 3 台 VM を立ち上げます。

Vagrantfile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Vagrant.configure("2") do |config|
  # ubuntu precise
  config.vm.box = "precise64"

  config.vm.define :proxy do |c|
    c.vm.network :private_network, ip: '192.168.55.55'
  end
  config.vm.define :web1 do |c|
    c.vm.network :private_network, ip: '192.168.55.56'
  end
  config.vm.define :web2 do |c|
    c.vm.network :private_network, ip: '192.168.55.57'
  end
end
  • proxy * 1: HAProxy, Synapse, Serf を動かします
  • web * 2: nginx, Serf を動かします

Web サーバのセットアップ

Web サーバには nginx をいれて適当に静的ファイルをおいておきます。

1
2
3
4
5
6
$ vagrant ssh web1
(web1) $ sudo apt-get install nginx
(web1) $ sudo /etc/init.d/nginx start
(web1) $ sudo sh -c "echo 'web1' > /usr/share/nginx/www/index.html"
(web1) $ curl http://localhost
web1
1
2
3
4
5
6
$ vagrant ssh web2
(web2) $ sudo apt-get install nginx
(web2) $ sudo /etc/init.d/nginx start
(web2) $ sudo sh -c "echo 'web2' > /usr/share/nginx/www/index.html"
(web2) $ curl http://localhost
web2

Serf を入れます。

1
2
3
(web1, web2) $ cd ~
(web1, web2) $ wget https://dl.bintray.com/mitchellh/serf/0.5.0_linux_amd64.zip
(web1, web2) $ unzip 0.5.0_linux_amd64.zip

Proxy サーバのセットアップ

rbenv で Ruby 2.1.1 をいれます。

1
(proxy) $ curl https://gist.githubusercontent.com/ryotarai/c2e6df6185fc262e0986/raw/install_rbenv_debian.sh | bash

HAProxy, Serf を入れます。

1
2
3
(proxy) $ sudo apt-get install haproxy
(proxy) $ sudo sed -i -e 's/ENABLED=0/ENABLED=1/' /etc/default/haproxy
(proxy) $ sudo /etc/init.d/haproxy start
1
2
3
(proxy) $ cd ~
(proxy) $ wget https://dl.bintray.com/mitchellh/serf/0.5.0_linux_amd64.zip
(proxy) $ unzip 0.5.0_linux_amd64.zip

Serf の join-member, leave-member イベントを受けて、サーバ一覧を更新するスクリプトをおいておきます。 以下の内容を~/serf-event-handlerとして保存します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#!/usr/bin/env ruby
require 'fileutils'

# proxy 以外のサーバの場合実行しない
exit 0 unless ENV['SERF_SELF_ROLE'] == 'proxy'

def memberships
  $stdin.each_line.map do |line|
    name, address, role, tags = line.split(' ')
    tags = Hash[tags.split(',').map {|kv| kv.split('=') }]
    {name: name, address: address, role: role, tags: tags}
  end
end

server_list_file = File.expand_path(ENV['SERVER_LIST'])

webservers = memberships.select do |membership|
  membership[:role] == 'web'
end

case ENV['SERF_EVENT']
when 'member-join'
  open(server_list_file, 'a') do |f|
    webservers.each do |webserver|
      f.puts("#{webserver[:address]} #{webserver[:tags]['port']}")
    end
  end
when 'member-leave'
  server_list_content = File.read(server_list_file)
  webservers.each do |webserver|
    escaped_address = Regexp.escape(webserver[:address])
    server_list_content.sub!(/^#{escaped_address} .+$\n/, '')
  end
  new_server_list_file = '/tmp/new_server_list'
  open(new_server_list_file, 'w') do |f|
    f.write(server_list_content)
  end
  FileUtils.mv(new_server_list_file, server_list_file)
end

実行可能にしておきます。

1
(proxy) $ chmod +x ~/serf-event-handler

Synapse 用の 設定ファイルを置いておきます。 以下の内容を~/synapse.conf.jsonとして保存します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
{
    "services": {
        "web": {
            "default_servers": [],
            "discovery": {
                "method": "file",
                "path": "/home/vagrant/server.list"
            },
            "haproxy": {
                "port": 80,
            }
        }
    },
    "haproxy": {
        "reload_command": "/etc/init.d/haproxy reload",
        "config_file_path": "/etc/haproxy/haproxy.cfg",
        "global": [],
        "defaults": [],
        "do_writes": true,
        "do_reloads": true
    }
}

synapse を実行しておきます。

1
2
3
4
(proxy) $ touch ~/server.list
(proxy) $ cd /vagrant
(proxy) $ bundle install
(proxy) $ sudo bundle exec synapse -c ~/synapse.conf.json

Serf エージェントを実行しておきます。

1
(proxy) $ SERVER_LIST=$HOME/server.list ./serf agent -node=proxy -bind=192.168.55.55 -tag role=proxy -event-handler=$HOME/serf-event-handler

Web サーバを Proxy のバックエンドに追加する

準備ができたので、実際に Web サーバをバックエンドに追加します。

web1 で Serf エージェントを起動して、クラスタに参加させます。 ここで指定したタグはイベントハンドラに渡され、ポート番号として使われます。

1
(web1) $ ./serf agent -node=web1 -bind=192.168.55.56 -join=192.168.55.55 -tag role=web -tag port=80

proxy で状態を確認します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
(proxy) $ cat server.list
192.168.55.56 80
(proxy) $ cat /etc/haproxy/haproxy.cfg
# auto-generated by synapse at 2014-03-31 15:13:01 +0000

global
defaults

frontend web
  bind localhost:80
  default_backend web

backend web
  server 192.168.55.56:80 192.168.55.56:80
(proxy) $ curl http://localhost
web1
(proxy) $ curl http://localhost
web1

web1 がバックエンドに追加されていることがわかります。

続いて、web2 をクラスタに追加します。

1
(web2) $ ./serf agent -node=web2 -bind=192.168.55.57 -join=192.168.55.55 -tag role=web -tag port=80

proxy をみてみます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
(proxy) $ cat server.list
192.168.55.56 80
192.168.55.57 80
(proxy) $ cat /etc/haproxy/haproxy.cfg
# auto-generated by synapse at 2014-03-31 15:15:39 +0000

global
defaults

frontend web
  bind localhost:80
  default_backend web

backend web
  server 192.168.55.56:80 192.168.55.56:80
  server 192.168.55.57:80 192.168.55.57:80
(proxy) $ curl http://localhost
web1
(proxy) $ curl http://localhost
web2
(proxy) $ curl http://localhost
web1

web2 がバックエンドとして追加されました。

ここで web1 の Serf エージェントを終了して、クラスタから除きます。

1
2
(web1) $ ./serf agent ...
(Ctrl + C)

proxy を確認すると、web1 がバックエンドから除かれていることがわかります。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$ cat server.list
192.168.55.57 80
$ cat /etc/haproxy/haproxy.cfg
# auto-generated by synapse at 2014-03-31 15:18:37 +0000

global
defaults

frontend web
  bind localhost:80
  default_backend web

backend web
  server 192.168.55.57:80 192.168.55.57:80
(proxy) $ curl http://localhost
web2
(proxy) $ curl http://localhost
web2

まとめ

Serf + Synapse を使うことで、Web サーバを立ち上げて Serf を立ち上げるだけで、プロキシ側を触ることなくサービスに投入することができます。また、Web サーバを落とすだけで、バックエンドから自動的に除かれます。 Disposable Infrastructure と相性がよさそうですね。

実際、Synapse を使わなくても、Serf のイベントハンドラを使うことで HAProxy の設定ファイルを動的に更新することは可能なので、Synapse を使うメリットがあるかは賛否がわかれるところかもしれません。

Chef のログを Fluentd に流す

Developers Summit 2014 での発表スライド)後、Chef のログを Fluentd に投げてる部分についてツイートをいただいたので、すこしまとめておきます。

Report (Exception) Handler

Chef の Report(Exception) Handler という仕組みを使って、ログを投げています。

  • Report Handler: Chef の実行が成功した場合に実行されるハンドラ
  • Exception Handler: Chef の実行が失敗した場合に実行されるハンドラ

詳しくは About Handlers を参照してください。

実装

実装としてはdataをそのまま投げるというものです。
datarun_statusをハッシュで表現したもので、ノードのアトリビュートや例外の情報などいろいろな情報を含んでいます。

ハンドラは以下の様な実装になります。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
require 'fluent-logger'

class FluentdHandler < Chef::Handler
  def initialize(tag_prefix, opts)
    @opts = opts
    @tag_prefix = tag_prefix
  end

  def report
    tag = success? ? 'report' : 'exception'
    logger.post(tag, data)
  end

  private
  def logger
    @logger ||= Fluent::Logger::FluentLogger.new(
      @tag_prefix, host: @opts[:host], port: @opts[:port]
    )
  end
end

このハンドラを使うにはsolo.rbなどで以下のようにハンドラを指定します。

1
2
3
handler = FluentHandler.new('chef', host: 'localhost', port: 24224)
report_handlers << handler
exception_handlers << handler

Gem 化しました

この記事を書くついでに Gem 化しておきましたので、ぜひお使いください。
(Pull Request お待ちしております)

GitHub: ryotarai/chef-handler-fluentd

MongoDB に保存する場合

fluent-plugin-mongo を使って MongoDB に保存する場合、dataのキーによっては BSON として invalid になってしまいます。
そのため、fluent-loggerに投げる前にキーに含まれる.と先頭の$を他の文字列に置き換える必要があります。

(おまけ)標準出力も投げる

Chef::Handler#dataで取得できるハッシュではコマンド実行時の標準出力などは取得できません。
そのため、標準出力は別途取得しています。

が、いろいろツライコードなので、もうちょっと良い実装方法はないものかと悩んでいます…


追記 2014/02/14 18:55

せっかく Fluentd, MongoDB つかってるのに全く構造化されてないデータ流すのナンセンスですね。


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
$logger_io = StringIO.new

class Chef
  module Mixin
    module ShellOut
      class MultiIO < BasicObject
        attr_reader :ios
        def initialize
          @ios = []
        end
        private
        def method_missing(*args)
          @ios.each do |io|
            io.__send__(*args)
          end
        end
      end

      # This overrides Chef::Mixin::ShellOut#shell_out
      # FIXME
      def shell_out(*command_args)
        cmd = Mixlib::ShellOut.new(*run_command_compatible_options(command_args))
        if STDOUT.tty? && !Chef::Config[:daemon] && Chef::Log.debug?
          multi_io = MultiIO.new
          multi_io.ios << STDOUT
          multi_io.ios << $logger_io
          cmd.live_stream = multi_io
        end
        cmd.run_command
        cmd
      end
    end
  end
end

# この行は Chef::Application#configure_logging のあとに実行する必要がある
Chef::Log.loggers << Logger.new($logger_io)

# ハンドラ
class FluentHandler < Chef::Handler
  def initialize(host, port, tag_prefix)
    @host = host
    @port = port
    @tag_prefix = tag_prefix
  end

  def report
    tag = success? ? 'report' : 'exception'
    logger.post(tag, data_to_post)
  end

  private
  def logger
    @logger ||= Fluent::Logger::FluentLogger.new(@tag_prefix, host: @host, port: @port)
  end

  def data_to_post
    data.tap do |d|
      d['stdout'] = $logger_io.string
    end
  end
end

Capistrano-rsync をつかってみた

デプロイ先から Git サーバにアクセスさせたくないという要求があって、capistrano-rsync を使ってみました。ちょっとハマったのでメモ。

環境

  • ruby 2.1.0
  • capistrano v3.1.0
  • capistrano-rsync v1.0.2

まず

Gemfile に適当に突っ込んでおく。

1
2
3
4
group :development do
  gem 'capistrano'
  gem 'capistrano-rsync'
end

基本的には普通に capistrano v3 の設定をする。

はまったところ

scm を rsync に

capistrano-rsync を使ってデプロイするためにconfig/deploy.rbで scm を rsync にする。

1
set :scm, :rsync

キャッシュ用ディレクトリの作成

このまま実行してみると、デプロイ先にキャッシュ用ディレクトリがなくて落ちるので、deploy:check:directoriesのあとに作成するようにする。

1
2
3
4
5
6
7
8
9
10
11
namespace :deploy do
  namespace :check do
    task :directories_for_rsync do
      on release_roles :all do
        execute :mkdir, '-pv', File.join(fetch(:deploy_to), fetch(:rsync_cache))
      end
    end
  end
end

after 'deploy:check:directories', 'deploy:check:directories_for_rsync'

rsync_options

capistrano-rsync の README には、

Set some rsync_options to your liking: set :rsync_options, %w[--recursive --delete --delete-excluded --exclude .git*]

と書いてあるのですが、rsync_optionsが設定できなくて苦労しました。どうやらデフォルトに上書きされてしまっているっぽい、ということでrsync_optionsを使う直前で設定するようにしました。

(自分が capistrano の理解が甘いだけな気もするわけですが)

1
2
3
4
5
6
7
namespace :rsync do
  task :set_options do
    set :rsync_options, %w[--recursive --delete --delete-excluded --exclude .git*]
  end
end

before 'rsync', 'rsync:set_options'

まとめ

capistrano-rsync 便利!特に大きなプロジェクトをデプロイするときとか重宝しそうですね。

GitHub TrendsのRSS Feedをつくりました

GitHub TrendsをRSS Feedとして取得できるGitHub Trends RSSをつくりました。

同様のサービスとしてhttp://github-trends.oscardelben.com/があったのですが、現在動作しておらず、GitHubのレポジトリにも

NOTE: this code no longer works since Github upgraded their explore page making scraping more difficult.

と書いてあり、対応しなさそうな雰囲気だったので、つくってみました。

dockerで起動したUbuntu 12.04にsshを通す

簡単に手順をまとめると

  1. Dockerfileにimageのつくりかたを書く
  2. docker buildでimageをビルドする
  3. 2でつくったimageにdocker tagで名前をつける
  4. sshでつなぐためにIPアドレスを調べる
  5. docker runでcontainerを起動
  6. docker portで起動したcontainerの22番ポートはどのポートからNATされているか調べる
  7. 4,6で調べたIP, portにsshで接続する
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
$ cat Dockerfile
FROM ubuntu:12.04
MAINTAINER Ryota Arai "[email protected]"

RUN apt-get update
RUN apt-get install -y openssh-server
RUN mkdir /var/run/sshd
RUN bash -c 'echo "root:root" | chpasswd'

CMD /usr/sbin/sshd -D
EXPOSE 22

$ docker build .
...
Step 5 : RUN mkdir /var/run/sshd
 ---> Running in e5e76f5de639
 ---> 92522331a6d7
Step 6 : RUN bash -c 'echo "root:root" | chpasswd'
 ---> Running in 7bdd5904cfdc
 ---> e93182966cc0
Step 7 : CMD /usr/sbin/sshd -D
 ---> Running in 1d978643a6e3
 ---> 455c7944ecc7
Step 8 : EXPOSE 22
 ---> Running in 3db08698551a
 ---> 88568c47ff16
Successfully built 88568c47ff16

$ docker tag 88568c47ff16 ryotarai/sshd
$ ip addr | grep -A4 docker0:
4: docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP
    link/ether 8a:cd:f7:fb:52:b0 brd ff:ff:ff:ff:ff:ff
    inet 172.16.42.1/24 scope global docker0
    inet6 fe80::f457:bff:fe76:ae7d/64 scope link
       valid_lft forever preferred_lft forever

$ ssh [email protected] -p $(docker port $(docker run -d ryotarai/sshd) 22)
The authenticity of host '[172.16.42.1]:49166 ([172.16.42.1]:49166)' can't be established.
ECDSA key fingerprint is 42:af:ab:c1:57:1c:a1:76:6f:ff:f6:b4:84:40:5a:dd.
Are you sure you want to continue connecting (yes/no)? yes
Warning: Permanently added '[172.16.42.1]:49166' (ECDSA) to the list of known hosts.
[email protected]'s password:
Welcome to Ubuntu 12.04 LTS (GNU/Linux 3.8.0-26-generic x86_64)

 * Documentation:  https://help.ubuntu.com/

The programs included with the Ubuntu system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Ubuntu comes with ABSOLUTELY NO WARRANTY, to the extent permitted by
applicable law.

-bash: warning: setlocale: LC_ALL: cannot change locale (en_US)
[email protected]:~#

OpenCVのJavaバインディングをJRubyから使ってみた

OpenCVのJava Bindingが登場したので、JRuby (1.7.3)から使ってみました。

OpenCV now supports desktop Java | OpenCV によるとC++のヘッダーをパースしてバインディングを生成するので、常に最新のAPIを利用することができるようです。

OpenCVをビルド

まず、OpenCV 2.4.4以降をビルドします。手動でビルドする場合は、Introduction to Java Development — OpenCV 2.4.4.0 documentationを参考に。

とりあえず、HomebrewでOpenCVをインストールしました。

$ brew update
$ brew install opencv

(すでにOpenCVがインストールされている場合)

$ brew upgrade opencv

Homebrewでインストールした場合、Javaバインディングは

/usr/local/Cellar/opencv/2.4.4a/share/OpenCV/java

に配置されます。(2.4.4aの場合)

ヘルパクラスを用意

動的ライブラリがうまく読み込めなかったので、JRubyでMeCabのJavaバインディングを利用しようとして、嵌まった。 – コンピュータわ難しくて分からない!!を参考にヘルパークラスを用意しました。

helpers/org/opencv/LdLibraryLoader.java

package org.opencv;;
import org.opencv.core.Core;

public class LdLibraryLoader {
    static {
        System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
    }
}

このファイルをJRubyから読み込むため、classファイルを生成します。(CallingJavaFromJRuby · jruby/jruby Wiki

$ javac -cp /usr/local/Cellar/opencv/2.4.4a/share/OpenCV/java/opencv-244.jar helpers/org/opencv/LdLibraryLoader.java

JRubyから使ってみる

Javaバインディングのリファレンスはhttp://docs.opencv.org/java/で閲覧出来ます。(2013/03/26 21:35追記)
試しにSIFT特徴を描画してみます。

$CLASSPATH << "helpers"
require 'java'
require '/usr/local/Cellar/opencv/2.4.4a/share/OpenCV/java/opencv-244.jar'

Java::org.opencv.LdLibraryLoader

java_import org.opencv.core.Mat
java_import org.opencv.core.MatOfKeyPoint
java_import org.opencv.highgui.Highgui
java_import org.opencv.features2d.FeatureDetector
java_import org.opencv.features2d.Features2d

src = Highgui.imread(ARGV.first)
keypoints = MatOfKeyPoint.new
detector = FeatureDetector.create(FeatureDetector::SIFT)
detector.detect src, keypoints

keypoints.to_array.each do |keypoint|
  point = keypoint.pt
  puts "x: #{point.x}, y: #{point.y}"
end

dst = Mat.new
Features2d.drawKeypoints src, keypoints, dst

Highgui.imwrite "./feature_out.png", dst

定番のLenaさんの特徴点を描画してみました。

$ jruby ./draw_features.rb /path/to/lena.tif

Lena with Features

ソースコード

https://github.com/ryotarai/opencv_on_jruby

callbackやerrbackをとるfunctionからjQueryのPromiseオブジェクトをつくる

doSomethingAndSucceed = (text, callback, errback) ->
  setInterval ->
    callback text
  , 1000
doSomethingAndFailed = (text, callback, errback) ->
  setInterval ->
    errback text
  , 1000

jQuery.deferrable(doSomethingAndSucceed, "succeeded", "__callback__", "__errback__").done (text) ->
  console.log text
jQuery.deferrable(doSomethingAndFailed, "failed", "__callback__", "__errback__").fail (text) ->
  console.log text;

#=> succeeded
#=> failed

上のようなことをしたかった。

jQuery.extend
  deferrable: ->
    deferred = @Deferred()
    callback = ->
      deferred.resolve.apply deferred, arguments
    errback = ->
      deferred.reject.apply deferred, arguments
    func = Array.prototype.shift.apply(arguments)
    args = []
    for argument in arguments
      if argument == "__callback__"
        args.push callback
        continue
      if argument == "__errback__"
        args.push errback
        continue
      args.push argument
    func.apply func, args;
    deferred.promise()

doSomethingAndSucceed = (text, callback, errback) ->
  setInterval ->
    callback text
  , 1000

doSomethingAndFailed = (text, callback, errback) ->
  setInterval ->
    errback text
  , 1000


jQuery.deferrable(doSomethingAndSucceed, "succeeded", "__callback__", "__errback__").done (text) ->
  console.log text

jQuery.deferrable(doSomethingAndFailed, "failed", "__callback__", "__errback__").fail (text) ->
  console.log text;

(JsFiddle)

VeeWeeでVagrantのBase Boxをつくる(Debian 6.0.6)

http://vagrantbox.esでもいいんですが、勉強がてらつくってみた。
(あと、DropBoxでホストされているBoxとかは得体のしれない物感が、ちょっと)

VeeWeeをインストール

$ gem install veewee --no-ri --no-rdoc
$ rbenv rehash

テンプレート一覧を表示

$ ls "$(dirname $(gem which veewee))/../templates"

テンプレートからdefinitionを書き出す

$ veewee vbox define "mysqueeze64" "Debian-6.0.6-amd64-netboot"

ISOファイルをJAISTから取得する

$ diff -C5 definitions/mysqueeze64/definition.rb{.original,}
*** definitions/mysqueeze64/definition.rb.original  2013-01-18 20:46:54.000000000 +0900
--- definitions/mysqueeze64/definition.rb   2013-01-18 20:48:53.000000000 +0900
***************
*** 2,12 ****
    :cpu_count => '1',
    :memory_size=> '256',
    :disk_size => '10140', :disk_format => 'VDI', :hostiocache => 'off',
    :os_type_id => 'Debian_64',
    :iso_file => "debian-6.0.6-amd64-netinst.iso",
!   :iso_src => "http://cdimage.debian.org/debian-cd/6.0.6/amd64/iso-cd/debian-6.0.6-amd64-netinst.iso",
    :iso_md5 => "00585d63f8a560a73540bd718263319a",
    :iso_download_timeout => "1000",
    :boot_wait => "10", :boot_cmd_sequence => [
      '<Esc>',
      'install ',
--- 2,12 ----
    :cpu_count => '1',
    :memory_size=> '256',
    :disk_size => '10140', :disk_format => 'VDI', :hostiocache => 'off',
    :os_type_id => 'Debian_64',
    :iso_file => "debian-6.0.6-amd64-netinst.iso",
!   :iso_src => "http://ftp.jaist.ac.jp/pub/Linux/debian-cd/6.0.6/amd64/iso-cd/debian-6.0.6-amd64-netinst.iso",
    :iso_md5 => "00585d63f8a560a73540bd718263319a",
    :iso_download_timeout => "1000",
    :boot_wait => "10", :boot_cmd_sequence => [
      '<Esc>',
      'install ',

VirtualBoxイメージを作成

VirtualBoxが立ち上がり、自動的にインストールが進む

$ veewee vbox build mysqueeze64

Vagrant用のboxファイルにエクスポート

$ vagrant basebox export 'mysqueeze64'
[mysqueeze64] Error: We executed a shell command and the exit status was not 0
[mysqueeze64] - Command :vagrant package --base 'mysqueeze64' --output 'mysqueeze64.box'.
[mysqueeze64] - Exitcode :1.
[mysqueeze64] - Output   :
/Users/ryota/.rbenv/versions/1.9.3-p327/lib/ruby/1.9.1/rubygems/dependency.rb:247:in `to_specs': Could not find archive-tar-minitar (= 0.5.2) amongst [CFPropertyList-2.0.17, ...

とエラーが出たので、

$ gem install archive-tar-minitar --no-ri --no-rdoc

して、再度

$ vagrant basebox export 'mysqueeze64'

djangoでファイルアップロードのサイズ制限をViewごとに設定する

django/tests/regressiontests/file_uploads/uploadhandler.py at master · django/djangoを参考にサイズ制限ができるUploadHandlerを定義します。

from django.core.files.uploadhandler import FileUploadHandler, StopUpload

class QuotaUploadHandler(FileUploadHandler):
    def __init__(self, request=None, quota_mb=5):
        super(QuotaUploadHandler, self).__init__(request)
        self.total_upload = 0
        self.quota = quota_mb * 2**20

    def receive_data_chunk(self, raw_data, start):
        self.total_upload += len(raw_data)
        if self.total_upload >= self.quota:
            raise StopUpload(connection_reset=True)
        return raw_data

    def file_complete(self, file_size):
        return None

Django | File Uploads | Django documentation
今回はViewごとに制限を行いたいので、View内でUploadHandlerを設定します。

request.upload_handlersにUploadHandlerを追加することで、ViewごとにUploadHandlerを設定することができますが、request.upload_handlersへの操作はrequest.POSTやrequest.FILESにアクセスする前に行う必要があります

通常、CSRF対策としてトークンがPOSTパラメータに含まれており、ビュー関数が実行される前にrequest.POSTにアクセスされてしまいます。これを回避するために一時的にCSRF対策をオフ(@csrf_exempt)にします。

@csrf_exempt
def hoge_view(request):
    @csrf_protect
    def _hoge_view(request):
        # Process ...
        # Return HttpResponse object
        
    # アップロード可能サイズを5 MBに制限する
    request.upload_handlers.insert(0, QuotaUploadHandler(quota_mb=5))
    return _hoge_view(request)

oh-my-zshで一時的にプロンプトのGitのステータスを非表示にする

oh-my-zshを使うと、シェルのプロンプト部分にカレントディレクトリのGitレポジトリのブランチ名や変更があるかなどのステータスを表示することができます。
これは非常に便利な機能なのですが、ディスクアクセスに時間がかかる場合(sshfsなど)に反応が遅くなってしまいます。
(.git以下を見にいくため)
一時的にプロンプトのGitのステータスを非表示にできるようにしました。

テーマを作る

oh-my-zshには豊富なテーマがあります。
このテーマのなかにプロンプトの設定も書かれているので、Gitなしテーマを作ります。

$ cd ~/.oh-my-zsh/themes/
$ cp robbyrussell.zsh-theme robbyrussell-nogit.zsh-theme 

コピーしたrobbyrussell-nogitテーマを編集します。diffは以下。

$ diff robbyrussell.zsh-theme robbyrussell-nogit.zsh-theme
1c1
< PROMPT='%{$fg_bold[red]%}➜ %{$fg_bold[green]%}%p %{$fg[cyan]%}%c %{$fg_bold[blue]%}$(git_prompt_info)%{$fg_bold[blue]%} % %{$reset_color%}'
---
> PROMPT='%{$fg_bold[red]%}➜ %{$fg_bold[green]%}%p %{$fg[cyan]%}%c %{$fg_bold[blue]%}%{$fg_bold[blue]%} % %{$reset_color%}'
3,6d2
< ZSH_THEME_GIT_PROMPT_PREFIX="git:(%{$fg[red]%}"
< ZSH_THEME_GIT_PROMPT_SUFFIX="%{$reset_color%}"
< ZSH_THEME_GIT_PROMPT_DIRTY="%{$fg[blue]%}) %{$fg[yellow]%}✗%{$reset_color%}"
< ZSH_THEME_GIT_PROMPT_CLEAN="%{$fg[blue]%})"

つまり、ZSH_THEME_GIT_PROMPT_*の行と、PROMPT変数の $(git_prompt_info) を削除します。

これで新しいテーマが出来ました。

一時的にテーマを変更する

テーマ名の環境変数を変更した後、oh-my-zsh.shを再読み込みします。

$ ZSH_THEME="robbyrussell-nogit"; source $ZSH/oh-my-zsh.sh

~/.oh-my-zsh/custom/nogit.zshに以下のように書いておけば、

alias nogit="ZSH_THEME="robbyrussell-nogit";source $ZSH/oh-my-zsh.sh"
alias nogitoff="ZSH_THEME="robbyrussell";source $ZSH/oh-my-zsh.sh"
$ nogit
$ nogitoff

で切り替えられます。