ruby

2012年3月 7日 (水)

第1回 渋谷Edge Rails勉強会×株式会社ドリコム事例発表 + heroku de rails事例発表

しばらくRailsから離れてJavaを使って仕事をしていたが、次のプロジェクト
ではひょっとしたらRailsを使うかもということで、久し振りにRuby関係の勉強会
に参加

herokuでRails3.2

heroku上で一晩でWe love heroku というサイトを作成した話。

一晩でこのクオリティ!!

Railsのscaffoldbootstrap(Twitter社製のデザインが得意でない人向け
CSSフレームワーク)を使用とのこと。

heroku社の人曰く、実際にUSのheroku社でも話題にのぼったそう。

scaffoldは初心者向けの餌だと思っていた時期が私にもありました。。

twitter bootstrap railsを使ったら職が見つかり彼女も出来て背も3センチ伸びました。
の記事といい、@ppworksさんの突如あらわれた彗星感はぱないっす。

Rails 3.2ってどう変わるの?

主な変更点は、

  1. 開発モードのパフォーマンスが3倍よくなった
  2. 遅いクエリがあった場合は自動でExplain結果がログに出力されるようになった
  3. ログにタグをつけれるようになり、後で追いやすくなった。

その他は、マイナーな変更。そこもきっちり説明しきれるところはさすが@onkさん。

@onkさんの一押しはrake routesが見やすくなったところ。

私は、consoleにpryが設定できるようになったことに感動しました。
早くrails consoleで、lsやcdを使ったり、カラーリングされたデータを見てみたいものです。

@onkさんはこの発表前に3000を越えるcommitをチェックしたという。

スゴイ。

Rails(Rubyも?)わずか3年でここまでいくとは。。。

というか逆にそこまでしているからすごいんですね。

勉強になりました。

MongoDB使わないんですかと尋ねたら、上手にデータ設計するまで2~3作
かかるだろうから、今はその余裕はないなとおっしゃっていた。
スルドイ。

ソーシャルアプリで使うjpmobileとガラケーからスマホへの対応

jpmobileがあれば、smartphoneも簡単に対応できるよという話。
どうすればいいかは、
Ruby on Rails 3で作る-jpmobileによるモバイルサイト構築

を読めということでした。
ガラケー用のソーシャルアプリのスマートフォン対応は、flashの
置き換えの工数のほうが全然大変だそうです。

まとめ

なんか、スライド終わりってなったら、実はまだ続きがあるっていう手法は
ドリコムで流行っているんでしょうか?
フリーフード、フリードリンクって40名から240名に増えたこともあって期待して
いなかったんですが、フードは瞬殺でしたが、ドリンクは豊富にありました。
調子にのって缶ビール3本もいただきました。
hatchupさん、GMOさん、ドリコムさんありがとうございました。
発表のレベルが高く、次回以降も楽しみです。

| | コメント (0) | トラックバック (0)

2011年11月 6日 (日)

rubyによるAmazon EC2のインスタンスのセットアップ

Amazon EC2を使う利点

各種APIが公開されているため、プログラマブルにインスタンスのセットアップができるところにつきると思います。

他のクラウド(もちろんAmazon EC2も)は、Webブラウザ上でインスタンスの増減やロードバランサ等の設定ができますが、手作業が必要なため自動でスケーリングができません。

また、もしデータセンター(amazonではゾーンと呼ばれる)全体が障害に陥いったとしても、セットアップをプログラムで自動化しておけば、ゾーンのURLを書き換えて実行するだけで、同じ環境を違うゾーンに簡単に復元できます。

Amazon EC2 セットアップスクリプト

ubuntuの素のOSから、ユーザを作成し、memcachedをインストール設定、起動するまでのスクリプトを書いてみました。
使用しているライブラリは公式のruby sdkではなく、https://github.com/appoxy/aws を使っています。
理由は公式よりも機能が豊富だったから。
スクリプトは、インスタンスに、debian系のイメージを使用することが前提になっています。
なおこのスクリプト作成には、SoftwareDesign 2010年10月号 クラウド活用プログラミング入門 第5章 自動スケールアウトに挑戦という山崎 泰宏さんの記事をかなり参考にしました。

AWSのインストール

gem install aws

config.yml

admin_user: "ubuntu"
application_user: "app"
aws_access_key: "AWS ACCESS KEY"
aws_secret_key: "AWS SECRET KEY"
key_name: "ubuntu"
image_id: "ami-e49723e5"
instance_type: "m1.small"
pem_path: "ubuntu.pem"
key_pub: "ssh-rsa AAAAB3N?????????????????"

ec2_setup.rb

#!/usr/bin/env ruby
require 'rubygems'
require 'aws'
require 'yaml'
require 'json'
require 'open3'

def create_ec2
  #EC2のインスタンスをTokyo regionで作成するように設定
  ENV["EC2_URL"] = "https://ec2.ap-northeast-1.amazonaws.com/"
  Aws::Ec2.new($config["aws_access_key"],$config["aws_secret_key"])
end

#EC2起動
def launch_instances(ec2)
    response = ec2.launch_instances($config["image_id"],
    {
      :min_count => $config["min_count"]||1,
      :max_count => $config["max_count"]||1,
      :user_data => $config["user_data"],
      :group_ids => [$config["security_group"]||"default"],
      :key_name => $config["key_name"],
      :instance_type => $config["instance_type"],
      :addressing_type       => nil,
      :kernel_id             => nil,
      :ramdisk_id            => nil,
      :availability_zone     => "ap-northeast-1a",
      :block_device_mappings => nil,
      :monitoring_enabled   => true
    })

    #作成したインスタンスのIDの配列を取得
    response.map{|instance| instance[:aws_instance_id]}
end

#インスタンスの起動完了を待つ
def wait_running(ec2,instance_ids)
  counter = 0
  unstart=true
  instance_dns_map = {}
  while(unstart)
    sleep 3
    counter += 1
    puts "*Status check...(#{counter})"
    instances = ec2.describe_instances(instance_ids)
    unstart = false
    instances.each do |instance|
      puts " Instance: #{instance[:aws_instance_id]} is #{instance[:aws_state]}"
      if instance[:aws_state] == "running"
        instance_dns_map[instance[:aws_instance_id]] = instance[:dns_name]
      else
        unstart = true
      end
    end
  end
  puts "All instances are running."
  instance_dns_map
end

#know_hostsから削除
def remove_known_hosts(hosts)
  f = "#{ENV['HOME']}/.ssh/known_hosts"
  ssh_hosts = File.read(f)
  new_rec = ""
  ssh_hosts.each_line do|el|
    rec = el
    hosts.each{|host| rec = "" if el =~ /#{host}/}
    new_rec += rec
  end
  File.open(f,"w"){|f| f.write(new_rec)}
end

#コマンドを実行する(system関数使わないのは、標準出力や標準エラー出力を受けとりたいため。)
def act_command(command,env={})
  puts command
  out = err = status = nil
  Open3.popen3(env,command) do|stdin, stdout, stderr, wait_thr|
    stdin.close
    out = stdout.read
    puts out if out && out != ''
    err = stderr.read
    if !err || err != ''
      STDERR.puts(err)
    end
    status = wait_thr.value
  end
  [status.to_i,out,err]
end

#sshコマンドを発行する StrictHostKeyCheckingのオプションは初回アクセスで未登録のホストに対してもエラーを出さない
def ssh_command(host,command)
  ssh = "ssh -o StrictHostKeyChecking=no -i #{$config['pem_path']} #{$config['admin_user']}@#{host} '#{command}'"
  act_command(ssh)
end

#scpコマンドを実行 directionはtoはリモートにcopy.それ以外はリモートからコピー
def scp(host,orig,dest,direction="to")
  if direction == "to"
    dest = "#{$config['admin_user']}@#{host}:#{dest}"
  else
    orig = "#{$config['admin_user']}@#{host}:#{orig}"
  end
  scp = "scp -o StrictHostKeyChecking=no -i #{$config['pem_path']} #{orig} #{dest}"
  act_command(scp)
end

#SSH接続ができるようになるまで待つ
def wait_ssh_up(instance_dns_map)
  counter = 0
  unstart=true
  while(unstart)
    sleep 3
    counter += 1
    puts "*SSH Connection check...(#{counter})"
    unstart = false
    instance_dns_map.each do |k,v|
      begin
        code = out = err = nil
        timeout(3) do
          code,out,err = ssh_command(v,'echo "success"')
        end
        if err && err =~ /REMOTE HOST IDENTIFICATION HAS CHANGED/
          act_command("ssh-keygen -R #{v}")
          unstart = true
        elsif code == 0
          puts " Try to ssh to an instance #{k}:#{v} Success."
          unstart = false
        else
          puts " Try to ssh to an instance #{k}:#{v} Failure."
          unstart = true
        end
      rescue Timeout::Error => e
        puts " Try to ssh to an instance #{k}:#{v} Timeout."
        unstart = true
      end
    end
  end
  puts "All instances ssh ready."
end

#アプリケーション用ユーザの作成
def user_setup(host)
  ssh_command(host,"sudo useradd -d /home/#{$config['application_user']} #{$config['application_user']}")
  ssh_command(host,"sudo mkdir -p /home/#{$config['application_user']}/.ssh")
  ssh_command(host,"sudo echo \"#{$config['key_pub']}\" > /tmp/authorized_keys")
  ssh_command(host,"sudo mv /tmp/authorized_keys /home/#{$config['application_user']}/.ssh/")
  ssh_command(host,"sudo chown -R #{$config['application_user']}:#{$config['application_user']} /home/#{$config['application_user']}")
end

#memcachedのインストール
def memcached_setup(host)
  ssh_command(host,"sudo mkdir -p /mnt/memcached/log")
  ssh_command(host,"sudo chown -R #{$config['application_user']}:#{$config['application_user']} /mnt/memcached")
  #質問に対して全部yesで返答
  ssh_command(host,"DEBIAN_FRONTEND=noninteractive sudo apt-get install --assume-yes memcached")
  scp(host,File.expand_path("../memcached.conf",__FILE__),"/tmp/memcached.conf")
  ssh_command(host,"sudo mv /tmp/memcached.conf /etc/memcached.conf")
  ssh_command(host,"sudo /etc/init.d/memcached restart")
end
#==========#
# MAIN処理 #
#==========#
#設定ファイルの読み込み
$config = YAML.load_file(File.expand_path("../config.yml", __FILE__))
keys = %w(admin_user application_user aws_access_key aws_secret_key
        key_name image_id instance_type pem_path key_pub)
if (remain_keys = keys - $config.keys) != []
  raise "config error: lack of #{remain_keys.join(',')} settings"
end
#EC2マネージャインスタンス作成
ec2 = create_ec2

#EC2インスタンス作成
instance_ids = launch_instances(ec2)

#インスタンスの起動完了を待つ
instance_dns_map = wait_running(ec2,instance_ids)

#以前に同じアドレスの違うインスタンスにログインしている可能性があるため、know_hostsから削除
remove_known_hosts(instance_dns_map.values)

#SSH接続ができるようになるまで待つ
wait_ssh_up(instance_dns_map)

instance_dns_map.each{|k,v|
  #アプリケーション用ユーザの作成
  user_setup(v)
  #memcachedのインストール
  memcached_setup(v)
}

memcached.conf

-d
logfile /mnt/memcached/log/memcached.log
# memory
-m 1440
# Run the daemon as root. The start-memcached will default to running as root if no
# -u command is present in this config file
-u app
-l 0.0.0.0

config.yml

admin_user
用意されている初期ユーザ 。使用するamiによって異ります。ubuntuの公式のイメージだとubuntuですが、イメージによってrootだったり、bitnamiだったりと異るため注意が必要です。

application_user

インスタンスのサーバのアプリを実行するユーザ。

aws_access_key

AWSのアクセスキー(AWSのアカウントのセキュリティ証明書のページに記載されています

aws_secret_key

AWSのシークレットアクセスキー(AWSのアカウントのセキュリティ証明書のページの表示リンククリックで表示されます)

key_name

EC2の初期ログインに必要なキーペアの名前 Management ConsoleでEC2を選択して、Key pairsを選択して作成。awsのライブラリを使って作成することもできます。

image_id

作成するインスタンスのイメージID(ami)

instance_type

インスタンスの種類(m1.largeとかm1.small)

pem_path

keynameで指定したキーファイルへのパス

key_pub

自分の公開鍵の文字列 インスタンスのセットアップが終了すると、立ち上がったインスタンスにapplication_userの名前でログインできます。

min_count

立ち上げたい最小インスタンス数(任意)

max_count

立ち上げたい最大インスタンス数(任意)

ec2_setup.rb

create_ec2
デフォルトだとvirginiaのリージョンになるので、tokyoリージョンになるように設定。

launch_instances

availability_zoneの設定は、もしzone全体に障害が起きた時は、create_ec2のEC2_URLとこの値を変更します。
monitoring_enabledはセットすると、clowdwathを使って、詳細メトリックスが取れます。 モニタリングの値もawsのライブラリで簡単に取れるので、cron等でチェックして負荷が高い時は、 スケールアウトさせることも容易です。
min_countやmax_countで同じイメージのインスタンスを同時に複数立ち上げることができます。

wait_running

インスタンスをlaunchしても1〜5分ぐらいstatusがpendingのまま。pending時間はその時によってまちまち。

remove_known_hosts

インスタンスの上げ下げをしていると同じアドレスの別インスタンスにぶつかり、前回とfinger_printが 違うのでエラーになって、ssh接続ができなくなるのを避けるため。

wait_ssh_up

sleepをしながら、SSH接続が成功するまで待ち続ける。 StrictHostKeyCheckingのオプションは、初回のsshアクセスの際にknow_hostsに追加していいかのプロンプトを出させなくするため。

user_setup

初期ユーザはno passwordでsudoができて危険なため、ユーザを作成し、公開鍵も登録してセットアップ完了時にログインができるようにする

memcached_setup

sudoが使えるのでapt-getでいれればいいだけだが、依存パッケージがある場合はインストールしていいかのプロンプトが出力されてしまうので、DEBIAN_FRONTEND=noninteractiveの設定と--assume-yes のオプションをつけています。

memcached.conf

logfile-mの設定は必須。EC2のディスク容量のほとんどは、/mnt配下に割り当てられている。 例えばsmallは、160G中147Gが/mnt以下に割り当てられています。
memoryはデフォルト64Mなので、メモリが許す範囲に割り当てなおします。

まとめ

実際やってみると、簡単ということがわかると思います。 基本的には、アップデートの容易さや他のアプリでの使い回しも考えると、素のOSのamiを使って、サーバのインストール、設定までを行うスクリプトを作成するのが望ましいですが、Webサーバのような負荷に応じて頻繁にインスタンスの上げ下げをする場合は、必要なパッケージや設定がすでにインストールされた状態の独自にamiを作成することで、インスタンスの立ち上がりからサービス開始までの時間を短縮させる必要があると思います。

| | コメント (0) | トラックバック (0)

2010年4月 3日 (土)

あなたのスキルで飯は食えるか?に挑戦

朝、twitterで西尾泰三氏があなたのスキルで飯は食えるか? 史上最大のコーディングスキル判定という記事を作成したというつぶやきを見かけた。 これは是非挑戦せねば、と挑戦した結果です。 時系列は下記の通り

9:50 開始 3時間後の1時に花見に行く約束なので丁度いい。
9:55 聴牌かどうかを割り出すのではなく、1-9のどれかを足してアガリ
になるかを確認するプログラムにしたらいいととひらめく。
9:57 1牌頭4面子 順序はシュンツ、コウツ、ジャントウの順で確認する
のがいいだろうと思いつく
10:10 シュンツ優先にすると11222333のパターンが駄目に気づく。
10:25 シュンツもコウツもいける用に再帰でチェックがいいのではと思いつき、
コーディング開始
11:10 関数完成
11:30 標準入力から受け取ったり、エラー処理も含めて、一応完成 
11:40 再度問題のページを開いてよく問題文を読むと待ち牌だけでなく、面子の
構成やどういう状態で待ちなのかも表示しないといけないことに気づく。
13:00 アガリを確認するのではなく、やっぱりテンパイを確認しないといけない
のかと思い直し、そうなると再帰は厳しいのかといろいろプログラムを弄りながら
試行錯誤するも、花見の時間なので諦める。
18:00  帰宅。テンパイかどうかは3面子1雀頭、もしくは4面子頭待ちのいづれか
だと気づく。また頭+コウツ+シュンツと並び順を固定すれば、すでに調査した
組み合わせの場合はじけると気づく
18:30 コーディング完了 30分のオーバー。ただなんとなく花見の時も考えてた
から、もっとオーバーかも。

下記がプログラムです。(ruby)
 #アガルために必要な面子数
  NEED_MENTSU=4
  #牌パイに存在する最大数
  MAX_NUM=9
  #同一数の牌パイの最大存在数
  MAX_HAI=4

  $kumiawase = {}
######################################################################
#指定された数字の牌を指定した数字分増減した牌パイのハッシュを作成し返却
######################################################################
def haipai_mod(orig,k,count)
  haipai = orig.clone
  if haipai[k]
    haipai[k] += count
  else
    haipai[k] = count
  end
  #同じ数字の牌が0個以下になることはあり得ないため。
  if haipai[k] < 0
    throw "count < 0"
  end
  #同じ数字の牌が最大数以上になることはあり得ないため。
  if haipai[k] > MAX_HAI
    throw "count > #{MAX_HAI}"
  end
  #定された牌の数が0個になった場合はhashからその数字の牌の存在を削除
  if haipai[k] == 0
    haipai.delete(k)
  end
  return haipai
end
#####################################
#残りの牌を待ちの仕様にあわせた文字列
#####################################
def machi(haipai)
  str = "["
  haipai.keys.sort.each do|k|
    str += k.to_s * haipai[k]
  end
  str += "]"
end
###########################################
#聴牌の場合は、面子と待ちを仕様に沿って表示
###########################################
def disp_tenpai(haipai,idx,mentsu,atama,str)
  #頭待ちの時は必ず聴牌
  if (mentsu == 4 and atama == 0)
    #面子と待ちの牌
    str = str + machi(haipai) + "\n"
    print str
    return
  end
  #1面子足りない場合は残りの牌に1~9までの数字を試す
  if (mentsu == 3 and atama == 1)
    #面子と待ちの牌を予め取得
    str = str + machi(haipai) + "\n"
    #残り牌に1~9の牌を付加
    9.times do |i|
      hai = i+1
      #残り牌今回の牌を付加
      new_haipai = haipai_mod(haipai,hai,1);
      #一番低い数字の牌を取り出す
      k = new_haipai.keys.sort[0]
      #コウツもしくはシュンツか?
      if new_haipai[k] == 3 or
         (new_haipai[k] == 1 and new_haipai[k+1] and new_haipai[k+2])
        #予め取得した面子と待ちの牌を表示
        print str
        return
      end
    end
  end
  (idx .. MAX_NUM).each do |k|
    next unless haipai[k]
    ret = false
    #頭がなく、頭にできる牌が見つかった場合
    if atama == 0 and haipai[k] >= 2
      new_haipai = haipai_mod(haipai,k,-2);
      new_str = "(#{k}#{k})" + str
      #すでに調査した組み合わせ(頭+面子)の場合スキップ
      unless $kumiawase[new_str]
        $kumiawase[new_str] = true
        #頭を除いた残りの牌で再帰
        disp_tenpai(new_haipai,k,mentsu,1,new_str)
      end
    end
    #コウツにできる場合
    if haipai[k] >= 3
      new_haipai = haipai_mod(haipai,k,-3)
      new_str =  "(#{k}#{k}#{k})" + str
      #すでに調査した組み合わせ(頭+面子)の場合スキップ
      unless $kumiawase[new_str]
        $kumiawase[new_str] = true
        #コウツを除いた残りの牌で再帰
        disp_tenpai(new_haipai,k,mentsu+1,atama,new_str )
      end
    end
    #シュンツにできる場合
    if haipai[k+1] and haipai[k+2]
      new_haipai = haipai_mod(haipai,k,-1)
      new_haipai = haipai_mod(new_haipai,k+1,-1)
      new_haipai = haipai_mod(new_haipai,k+2,-1)
      new_str =  str + "(#{k}#{k+1}#{k+2})"
      #すでに調査した組み合わせ(頭+面子)の場合スキップ
      unless $kumiawase[new_str]
        $kumiawase[new_str] = true
        #シュンツを除いた残りの牌で再帰
        disp_tenpai(new_haipai,k,mentsu+1,atama,new_str)
      end
    end
  end
end

hai = ARGV[0]
if hai !~ /^\d{13}$/
  throw "you should put 13 number between 1 and 9."
end
orig_haipai = {}
#各数字をキーにして、キーに対して存在する牌数を値にしたハッシュを作成
hai.split('').each do|h|
  orig_haipai = haipai_mod(orig_haipai,h.to_i,1)
end
#聴牌確認
disp_tenpai(orig_haipai,1,0,0,"")

やっていて一番思ったことは、コーディングしながら考えるのは非常に効率が悪いということ。 どういう処理をするかを頭の中で概念的に整理してからコーディングした方が断然速い。 最初はそうしていたのだが、仕様をよく読んでなかったせいで、後でやり直しになった際に つい作ったプログラムを弄りながら考えてしまったせいで、全然進まなかった。 花見でコーディングから離れているうちに考えが思いつき、後のコーディングはあっと いうまだった。

| | コメント (0) | トラックバック (0)

2010年2月13日 (土)

Amazon S3にバックアップをする2

以前からrubyのgemcutterに登録しているs3backupというツールをこのところいろいろとBug修正や仕様を変更しました。仕様は変更しましたが、バージョンアップしても、今までのバックアップは失われず、設定ファイルもそのまま使用できます。
Amazon S3にバックアップする

Amazon S3の登録はクレジットカードがあれば、あっという間にできます。
値段は現在のところ下記の通りです。
Storage
$0.150 per GB - first 50 TB / month of storage used
Data Transfer
$0.000 per GB Internet Data Transfer - all data transfer into Amazon S3 (Until June 30, 2010)**
Requests
$0.01 per 1,000 PUT, COPY, POST, or LIST requests

300Gをバックアップするとして、今の為替だとだいたい月4500円ぐらい。
個人ユースには、ちょっと高い?
ただ、必要なファイルだけに絞れば、3Gだと45円。これならいつでもどこでも取り出せて、万一災害等で一切のバックアップメディアが失われたとしても、大丈夫なことを考えるとお得な気もします。

仕様の変更
1.sqlite3を使用するようになった。
バックアップ対象の各ファイル情報を今までHashのメモリでもっていたのに対して、sqlite3を使用するようにしました。理由は200Gぐらいあるディレクトリツリーをバックアップしたところ、4Gもメモリがあるにもかかわらず、メモリ不足で強制的にkillされるということがあったためです。そのため、sqlite3-rubyというgemに依存するようになりました。sqlite3-rubyはnative extentionを含むため、gccが必要になったり、sqlite3のdevel環境が必要になったりと少しインストールのハードルが上がりました。

2.レジューム機能を必ずするようになった。
以前はメモリを使用しすぎるため、resume機能をデフォルトでoffにしたのですが、sqlite3使用により、resume機能を付加してもメモリ消費が変わらなくなったため、常にonにしました。

3.一時ディレクトリを指定できるようになった。
以前は/tmpの下で圧縮や展開をしていたのですが、バックアップ対象のディレクトリのサイズが大きかった場合、/tmpでは賄えない場合に一時ディレクトリを/tmp以外にできるようにしました。

Bugの修正
1. パスワード設定をしていなかった場合、backupがエラーになるのを修正。
2. 特殊文字を含むディレクトリの場合、backupがエラーになるのを修正。
3.コネクションリセットのエラーによりbackupが中断されるのを修正。
4. ディレクトリ名と直下のサブディレクトリ名が同じ場合、そのディレクトリのバックアップが正常に行われないため、リストアに失敗するのを修正。
5.ディレクトリ名が長すぎる場合に、backupに失敗するのを修正。

| | コメント (0) | トラックバック (0)

2009年11月14日 (土)

MySQLのテーブル一覧、create table一覧、show index一覧、レコード数の一覧を取得する。

MySQLにおいて、指定したデータベースにあるすべてのテーブルのcreate tableの一覧や、
index情報の一覧やレコード数の一覧を作成するコマンドをrubyで作った。
rubyのmysqlのパッケージに依存するため、予め、下記のコマンドを実行してインストールしておく
必要がある。
sudo gem install mysql


使い方

table_info.rb -d データベース名 [-H ホスト名] [-P ポート番号] [-u ユーザー名][-p パスワード][-c][-i][-n]

-d データベース名 必須。
-H ホスト名 指定しなかった場合はlocalhost
-P ポート番号 指定しなかった場合は3306
-u ユーザー名 指定しなかった場合はroot 
-p パスワード 指定しなかった場合はなし。
-c create tableの一覧を出力
-i show indexの一覧を出力
-n レコード数の一覧を出力

-c,-i,-nは同時指定可能です。

#!/usr/bin/ruby
require 'rubygems'
require 'mysql'
require 'optparse'
options = {
  :database => '',
  :host=> 'localhost',
  :create=> false,
  :count=> false,
  :index=> false,
  :port=> 3306,
  :user=> 'root',
  :password => '',
}
begin
  parser = OptionParser.new do |opt|
    opt.banner = "Usage: #{File.basename($0)} [Option]"
    opt.on("-d","--database String","database name") { |o|
      options[:database] = o
    }
    opt.on("-H","--hostname String","hostname. default: localhost") {|o|
      options[:host] = o
    }
    opt.on("-n","--number","count record number. caution! don't use ovarload") {
      options[:count] = true
    }
    opt.on("-c","--create","show create table list.") {
      options[:create] = true
    }
    opt.on("-i","--index","show index from table list.") {
      options[:index] = true
    }
    opt.on("-P","--port Integer","port. default: 3306.") {|o|
      options[:port] = o
    }
    opt.on("-u","--user String","user name. default:root"){|o|
      options[:user] = o
    }
    opt.on("-p","--password String","password. default:''"){|o|
      options[:password] = o
    }
    opt.on("-h","--help","print this message and quit") {
      puts opt.help
      exit 0
    }
    opt.parse!(ARGV)
  end
rescue OptionParser::ParseError => err
  $stderr.puts(err.message)
  exit 1
end
if options[:database] == nil or options[:database] == ""
  $stderr.puts("no database name.")
  exit 1
end
my = Mysql.init()
#接続開始
db = Mysql::new(options[:host], options[:user], options[:password],options[:database],options[:port])
#SQL文の実行
st = db.query("show tables")
st.each do |tbl|
  puts "TABLE=#{tbl[0]}"
  if options[:create]
    st2 = db.query("show create table #{tbl[0]}")
    st2.each do |attr|
      printf "%s\n"%attr[1]
    end
  end
  if options[:index]
    st3 = db.query("show index from #{tbl[0]}")
    key_name = {}
    st3.each do |idx|
      unless key_name[idx[2]]
        key_name[idx[2]] = {} 
        key_name[idx[2]]["Cardinality"] = idx[6]
        if idx[1] == 1
          key_name[idx[2]]["Non_unique"] = "True"
        else
          key_name[idx[2]]["Non_unique"] = "False"
        end
        key_name[idx[2]]["Index_type"] = idx[10]
        key_name[idx[2]]["Column_name"] = []
      end
      key_name[idx[2]]["Column_name"].push idx[4]
    end
    next if key_name.size == 0
    key_data = []
    key_name.each do |key,val|
      if key == "PRIMARY"
        val["Key_name"] = key
        key_data.unshift val
      else
        val["Key_name"] = key
        key_data.push val
      end
    end
    key_data.each do |data|
      puts "key_name=>#{data['Key_name']}  Non_unique=>#{data['Non_unique']} "+
        "Cardinality=>#{data['Cardinality']} "+
        "Index_type=>#{data['Index_type']}  Column_name=>#{data['Column_name'].join(',')}"
    end
  end
  if options[:count]
    st4 = db.query("select count(*) from #{tbl[0]}")
    st4.each do |attr|
      printf "total count=%d\n"%attr[0]
    end
  end
  printf "\n\n"
end
#接続を切断する
db.close

| | コメント (0) | トラックバック (0)

2009年10月31日 (土)

windowsでgemで入れたコマンドがすべてin `load': no such file to load になる

WindowsのRuby実行環境としてActiveScriptRuby 1.8.7(p174) with-winsock2 
をインストール。
環境変数PATHにC:/Program Files/ruby-1.8/binを追加し再起動
(環境変数の設定を反映させるため。)

C:\>gem --version
1.3.4

C:\>gem install rake
Successfully installed rake-0.8.7
1 gem installed
Installing ri documentation for rake-0.8.7...
Installing RDoc documentation for rake-0.8.7...

C:\>rake --version
C:/Program Files/ruby-1.8/bin/rake:18:in `load': no such file to load --
C:/Program Files/ruby-1.8/lib/ruby/gems/1.8/gems/rake-0.8.7/bin/rake"
(LoadError)
        from C:/Program Files/ruby-1.8/bin/rake:18

疑問:
え!? rake 入れたばっかりなのに、なんでno such file?
答え:
実はgemのversion 1.3.4には実行ファイルへのパスにスペースが含めないバグ
があるせいで、実行できない。

疑問:
ruby consoleだと、rakeがちゃんと動作する。なんで!?
答え:
ruby consoleはコマンドプロンプト立ち上げ時に環境変数PATHの先頭に
C:\PROGRA~1\ruby-1.8\bin;を追加している。PROGRA~1という
ディレクトリ名にはスペースが含まれないため、gemのバグを回避している。

疑問:
C:\PROGRA~1って何?
答え:
8.3形式の短いファイル名を表示させる に記述されている通り、Windowsは、
昔のDOSに対応できるよう、ディレクトリやファイル名に自動的に8.3形式の
別名を割り振り、どちらの名前を指定しても同じ動作になる。

疑問:
で、rakeを実行するにはどうすればいいの?
答え:
・環境変数PATHにC:/Program Files/ruby-1.8/binではなく、
C:\PROGRA~1\ruby-1.8\binを追加する。(消極的解決法

・gemをversion1.3.5にアップデートする。(積極的解決法

疑問:
gemってどうやってアップデートするの?
答え:

C:\>gem install rubygems-update
Bulk updating Gem source index for:
http://gems.rubyforge.org/
Succeccfully installed rubygems-update-1.3.5
1 gem installed

C:\>set PATH=C:\PROGRA~1\ruby-1.8\bin;%PATH%

C:\>update_rubygems

新しいコマンドプロンプトを立ち上げ、rakeが実行できるか確認する。


C:\>rake --version
rake, version 0.8.7


CONGRATULATION!!

| | コメント (0) | トラックバック (0)

2009年10月25日 (日)

Amazon S3にバックアップをする

会社でサーバのファイル郡をAmazon S3にバックアップすることになり、その
ためのツールをRubyで作成しました。
会社の了解を得て、オープンソースにしていいことになったので、rubyforgeに
登録しました。ただ、Windowsでは動作しません。
S3Backup:http://rubyforge.org/projects/s3backup/

S3Backupについて

概要
S3Backupツールとは、指定したディレクトリ以下をAmazon S3にバックアップ
/リストアするためのツールです。

特徴としては下記があります。

圧縮
Amazon S3にアップロードする際に各ディレクトリを圧縮するため、通信帯域や保存領域を節約することができます。

暗号化
設定によりアップロードするファイルを暗号化させることもできます。

差分バックアップ
バックアップが前回バックアップした時と内容が変わらないディレクトリは、
アップロードしないため、通信帯域や時間を節約することができます。

レジューム
通信エラー等で途中でバックアップが中断されても、次回実行時に前回
送信に成功したディレクトリはほとんど再送信されません。(効率化のため
毎回ではなく、10ディレクトリに1回レジューム用の情報を更新しています。)

※注
ディレクトリの内容が変わったかどうかの判断は下記によります。
・ファイルの追加
・ファイルの削除
・ファイルの更新日付の変更
・ファイルのサイズの変更
・シンボリックファイルのリンク先の変更

インストール
sudo gem install s3backup

設定ファイル
S3Backupツールのバックアップ・リストアの実行には下記の内容がyamlフォーマットで書かれた設定ファイルを予め準備しておく必要があります。

bucket: "バケット名"
directories:
- "バックアップもしくはリストア対象のディレクトリへの絶対パス"
- "ディレクトリへの絶対パス(複数ある場合は以降続ける)"
access_key_id: 'Amazon access_key_id'
secret_access_key: 'Amazon secret_access_key'
password: 'ファイルを暗号化する際のパスワード(任意)'
salt: '16文字のHex文字列(8Byte)passwordが指定されている場合のみ必要'
buffer_size:バッファーサイズのbyte数(任意)
max_retry_count送信失敗時に再送する回数(任意)
proxy_host:プロキシサーバのアドレス(任意)
proxy_port:プロキシサーバのポート(任意)
proxy_user:プロキシサーバのログイン名(任意)
proxy_password:プロキシサーバのパスワード(任意)
log_level:
ログの出力レベル(任意)
temporary: 圧縮・展開用の一時ディレクトリ(任意)

設定の説明
bucket

任意のファイルを登録できるコンテナ。指定されたbucketがなかった場合は作成される。Amazon S3内で一意である必要。

directories
バックアップもしくはリストアしたいディレクトリ。複数指定できる。またリストアの場合はバックアップされているディレクトリの配下なら任意のディレクトリを指定できる。リストア時にdirectoriesを指定しない場合は、指定したbucketにバックアップされたすべてのディレクトリがリストアされる。

access_key_id
Amazonより渡されたユーザーごとのid

secret_access_key
Amazonより渡されたユーザーに対する秘密のパスワード

password

AES-CBC-256形式でファイルを暗号化する際のパスワード。なお、設定されていない場合は暗号化しない

salt
AES-CBC-256形式でファイルを暗号化する際のSalt。16文字のHex文字列(8Byte)。passwordが指定されている場合のみ必要'

buffer_size
デフォルト32M ディレクトリを圧縮した後、BUF_SIZEに分割し、送信する。最大5G。

max_retry_count
デフォルト20 送信が失敗した際にリトライする回数。

proxy_host
プロキシサーバのアドレス(プロキシサーバ使用時のみ)。

proxy_port
プロキシサーバのポート(プロキシサーバ使用時のみ)。

proxy_user
プロキシサーバへのログイン名(プロキシサーバ使用時かつログイン必須の場合のみ)。

proxy_password
プロキシサーバへのログインのパスワード(プロキシサーバ使用時かつログイン必須の場合のみ)。

log_level
debug,info,warn,errorのどれかを選択することでログの出力レベルを変更する。デフォルトはinfo

temporary
バックアップ対象のディレクトリを圧縮した際にできる一時ファイルやファイル情報用の一時dbを置くディレクトリ。デフォルトは/tmp

コマンドシンタックス

バックアップの場合
s3backup
[-f 設定ファイル] [-v デバッグメッセージの出力] [-l ログのパス] [-h ヘルプメッセージ]

  • 設定ファイル 設定しない場合はカレントディレクトリのbackup.ymlを読み込む。
  • デバックメッセージ バックアップ対象のディレクトリのファイルツリー構成と更新があったファイル名を出力
  • ログのパス 設定しない場合は標準出力に出力されます
  • ヘルプ ヘルプメッセージが出力されます

リストアの場合
s3backup -r [-f 設定ファイル] [-v デバッグメッセージの出力] [-l ログのパス] [-o 出力ディレクトリ] [-h ヘルプメッセージ]

  • 設定ファイル 設定しない場合はカレントディレクトリのbackup.ymlを読み込む。
  • デバックメッセージ バックアップ対象のディレクトリのファイルツリー構成と更新があったファイル名を出力
  • ログのパス 設定しない場合は標準出力に出力されます
  • 出力ディレクトリ リストア先のディレクトリを指定します。 設定しない場合はカレントディレクトリに出力されます。
  • ヘルプ ヘルプメッセージが出力されます

2010/1/27 追記
本当すみません。passwordを設定していない場合に動作しませんでした。
あとCtrl-C等でバックアップを途中で中断した時に、次回続きから実行できるresume機能をオプションにし、デフォルトでoffにしました。(バックアップ対象が百Gを越えるディレクトリツリーの場合に、メモリを使いすぎるため)

2010/2/13 追記
レジューム機能を必須とするようにしたため、resumeの設定の記述の削除
ログの出力レベルを指定するlog_levelの設定を追加
新たに一時ディレクトリを指定するtemporaryの設定を追加

| | コメント (3) | トラックバック (0)

2009年6月19日 (金)

rake specが warning: useless use of == in void context

rspecのitの中で最後の文以外でsholud == を使うと 
warning: useless use of == in void context
のWarningが出力される。 

==が返すtrue,falseを使用していないよ、=と間違っている
んじゃない?というrubyの文法チェッカーの警告だと思われる。

別にWarningだけで、テストには影響ないのでいいのだが、ちょっと気持ち悪い。
気にしたいWarningを見落とすかもしれないし、できれば出力しないようにしたい。

should eqlだと上記warningがでないことを発見!
厳密に言うと==とeqlは再定義を行えば、違う可能性もあるけど、そうなることなんて
ほとんどないだろうから、これからは
should eql
で行こう!

| | コメント (2) | トラックバック (0)

2009年6月 6日 (土)

WindowsでSelenium RC + Rspec

Google App engineのPythonで作ったツールのテストをどうしようかなと思っていたら、業務でつかっているSeleniumがぴったりだと気づいた。
しかし、毎回ログインするのは面倒だったり、テストケースを日本語で表示したかったりということで、RSpec上にSeleniumを使った試験をWindows上に構築。
おまけに画面キャプチャもテスト中のプログラムからも行えるようにしてみた。
複雑そうに見えるこの作業も今や本当に簡単になったもんだと感動。

手順
1.下記のリンクからSelenium RCのダウンロードし、適当なディレクトリに展開
  http://seleniumhq.org/download/

2.RSpecのインストール

gem install rspec

3.SeleniumのRuby用アダプタ

gem install selenium-client

4.コンソールでRSpecの試験結果の色を表示するため。

gem install win32console

5.FireFoxのアドオンScreengrabを下記URLからインストール
 https://addons.mozilla.org/ja/firefox/addon/1146

6.下記の内容のRakefileを適当なディレクトリに作成

 # cofing: utf-8
require 'rubygems'
require 'spec/rake/spectask'

task :default => [:spec]

Spec::Rake::SpecTask.new do |t|
  t.warning = true
  t.rcov = false
  t.spec_opts << '--color' << '-fs'
end

7.6.で作ったRakefileのディレクトリの配下にspecという名前のディレクトリの作成

8.7.で作ったディレクトリに下記の内容のselenium_helper.rbを作成

 require 'date'
module SeleniumHelperMethods
  def startup
    @captures = {}
    i=1
    date_str=Date.today.to_s.gsub("-","")
    while File.exist?(File.dirname(__FILE__) + "/capture/" + date_str + "/" + i.to_s) do
      i+=1
    end
    @dir_name = File.dirname(__FILE__) + "/capture/" + date_str + "/" + i.to_s
    FileUtils.mkdir_p(@dir_name)
    @selenium_driver = Selenium::Client::Driver.new(
        :host => "localhost",
        :port => 4444,
        :browser => "*firefox",
        :url => "http://localhost:8080",
        :timeout_in_second => 80,
        :javascript_framework => :prototype)

    @selenium_driver.start_new_browser_session
    @selenium_driver.open "/"
    @selenium_driver.type "email", "test@example.com"
    @selenium_driver.click "//input[@value='Login']", :wait_for => :page
  end
  def capture
    @captures[self.description] = 0 unless @captures[self.description]
    @captures[self.description]+=1
    capture_name = NKF.nkf("-Sw",self.description) + "_" + @captures[self.description].to_s + ".png"
    @selenium_driver.capture_entire_page_screenshot(@dir_name + "/" + capture_name, "");
  end
end

Selenium::Client::Driver.newの内容は各自のアプリケーションに合わせて適宜修正してください。ここでは、GAEのローカルのテストを想定しています。

9.7.で作ったディレクトリ配下にRSpecのテストを適当_spec.rbの名前でutf-8の文字コードで作成(下記は私のアプリの例)

 #!/usr/bin/env ruby
#
# Sample Ruby script using the Selenium client API
#
$:.unshift File.expand_path(File.dirname(__FILE__))
require "rubygems"
require "spec"
require "nkf"
require "selenium/client"
require "selenium/rspec/spec_helper"
require "selenium_helper"

describe NKF.nkf("-Ws",%q|帳簿登録|) do
  attr_reader :selenium_driver
  alias :page :selenium_driver
  include SeleniumHelperMethods
  before(:all) do
    startup
  end
 
  after(:each) do
    capture
  end
  after(:all) do
    @selenium_driver.stop
  end
  it NKF.nkf("-Ws",%q|登録ボタンで一覧に表示|) do
    page.type 'category','food'
    page.type 'account_price','200'
    page.type 'account_content','パン'
    page.click '//input[@value="登録"]', :wait_for => :ajax
    page.table_cell_text('//table[@class="account_list"].1.1').should == '200円'
  end
end

上記のNKF.nkf("-Ws",%q|帳簿登録|)のくだりは、rspec実行時にコマンドプロンプト上ではUTF-8は文字化けしてしまうため、sjisに変換している。 またcaptureメソッド呼び出しにより、その時点の画面のキャプチャがitに指定した文言+連番で取得できる。一応各テスト終了ごとに自動的に画面キャプチャを呼び出すようにしている。

10.1.で展開したディレクトリの配下のselenium-server-1.0に移動し、下記のコマンド実行により、Selenium RCサーバの起動

java -jar selenium-server.jar

11. 6.で作ったRakefileがあるディレクトリに移動し、下記のコマンドでRSpecの実行

rake spec

以上

うまくいけば、下記のような快適な環境ができる。

コマンドプロンプト

D:\test>rake spec
(in D:/test)

帳簿登録
- 登録ボタンで一覧に表示

Finished in 15.891 second

1 example, 0 failures

登録ボタンで一覧に表示_1.png _1
登録ボタンで一覧に表示_2.png _2

| | コメント (0) | トラックバック (0)

2009年5月17日 (日)

ログからバイナリファイル変換

ログに出力した数字(10進数、16進数)をバイナリに変換するツール
バイナリに変換すれば、stiringなどのツールで各構造体のメンバにどんな
データが入っていたかを調べられたり、ascii以外の文字コードでもバイナリ
変換後にvim等のテキストエディタで読めるようになる。
使い方

log2bin.rb [-x] 出力ファイル
-x 入力が16進の場合

標準入力にバイナリ化したい文字列を渡す。
10進数は1~3桁、16進数は2桁の連続した数字を取り込み、それ以外
の文字列は無視。改行もOK
Ctrl-Dで入力終了

いつものように自己責任でお願いします。
log2bin.rb

 #!/bin/env ruby

$HEX = false
if ARGV[0] == "-x"
  $HEX = true
  ARGV.shift
end
if ARGV.length != 1
  puts "#{$0} outputfile"
  exit -1
end
unless file = File.open(ARGV[0],"wb")
  puts "#{$0} outputfile"
  puts "#{ARGV[0]} can't write"
  exit -1
end
begin
  datas = []
  if $HEX
    STDIN.each do |line|
      datas.concat(line.scan(/[\da-zA-Z]{2}/).map do|i| i.hex end)
    end
  else
    STDIN.each do |line|
      datas.concat(line.scan(/[\d]{1,3}/).map do|i| i.to_i end)
    end
  end
  file.write(datas.pack("C*"))
ensure
  file.close
end

| | コメント (0) | トラックバック (0)