pryとfogでお手軽マルチクラウド対応CLI

Rubyの高機能なREPLである[pry](https://github.com/pry/pry)とマルチクラウド対応ライブラリの[fog](http://fog.io/)を利用して、簡単に複数のクラウドを扱うことができます。

# TL;DR

* pryとfogを組み合わせるとクラウドの操作に補完・シンタックスハイライト・履歴・Rubyの配列操作などが使え非常に便利
* fogが提供しているインターフェースにより異なるサービスでも同じコマンドで操作できる
* 仮想マシンだけでなくオブジェクトストレージ、DNS、CDNも扱える

# 準備

pryとfogをインストールしておきます。

## ライブラリのインストール

```bash
$ gem install pry
$ gem install fog
```

## pryの設定

```~/.pryrc``` に下記を追記してpryからfogを使ってクラウドに接続するためのコマンドを追加します。

```rb
Pry::Commands.create_command "fog"  do
  description "Connect to the service provider using Fog"
  command_options :keep_retval => true

  banner <<-BANNER
    Usage:
    fog [OPTIONS] PROFILE  # Connect to the provider using the specified profile
    fog -l                 # list profiles
  BANNER

  def setup
    require 'yaml'
    require 'fog'
    @profiles = YAML.load_file(File.expand_path("~/.fog"))
  end

  def options(opt)
    opt.on :l, :list, 'List profiles'
    opt.on :r, :resource, 'Resource (compute, storage, dns, cdn)',
      :argument => true, :default => 'compute', :as => String
    opt.on :p, :provider, 'Provider', :argument => true, :as => String
    opt.on :n, :nosession,  'Return a connection object without starting session'
  end

  def process

    if opts[:list]
      @profiles.keys.each { |k| output.puts k }
      return
    end

    profile_name = args.first.nil? ? 'default' : args.first
    profile = symbolize(@profiles[profile_name])
    if profile.nil?
      output.puts "No such profile: #{args.first}"
      return
    end

    profile[:provider] = opts[:provider].nil? ? profile[:provider] : opts[:provider]

    connection = 
      case opts[:resource]
      when "storage"
        Fog::Storage.new(profile)
      when "dns"
        Fog::DNS.new(profile)
      when "cdn"
        Fog::CDN.new(profile)
      else
        Fog::Compute.new(profile)
      end

    if ! opts[:nosession]
      connection.pry :prompt => proc { |_, _, pry|
        "[#{pry.input_array.size}] fog[#{opts[:resource].downcase}:#{profile[:provider].downcase}:#{profile_name}]> "
      }
    end

    connection
  end

  private

  def symbolize(hash)

    return nil if hash.nil?

    hash.keys.each do |key|
      hash[(key.to_sym rescue key) || key] = hash.delete(key)
      return hash
    end
  end
end
```

## プロファイルの設定

APIのキーやトークンなどを毎回入力する手間を省くため```~/.fog```というファイルに接続情報を書いておきます。

ここでは特定のプロバイダに接続するときのアカウント情報をプロファイルと呼ぶことにします。
設定する必要がある項目はプロバイダ毎に異なりますが、基本的には以下のような構成になっています。

デフォルトではdefaultというプロファイル名が使用されます。

```yaml
プロファイル名:
  provider   : プロバイダー
  api_key    : API_KEY
  secret_key : SECRET_KEY
```

今回試してみた[IDCFクラウド](http://www.idcf.jp/cloud/)、[AWS](http://aws.amazon.com/jp/)、[DigitalOcean](https://www.digitalocean.com)では以下のようになります。
defaultを簡単に切り替えられるようアンカーとエイリアスを使用しています。

```yaml:~/.fog
---

idcfcloud: &idcfcloud
  provider:                     Cloudstack
  cloudstack_scheme:            https
  cloudstack_host:              compute.jp-east.idcfcloud.com
  cloudstack_path:              /client/api
  cloudstack_port:              443
  cloudstack_api_key:           YOUR_API_KEY
  cloudstack_secret_access_key: YOUR_SECRET_KEY

aws: &aws
  provider:              AWS
  aws_access_key_id:     YOUR_ACCESS_KEY_ID
  aws_secret_access_key: YOUR_SECRET_ACCESS_KEY

digitalocean: &digitalocean
  provider:               DigitalOcean
  digitalocean_api_key:   YOUR_API_KEY
  digitalocean_client_id: YOUR_CLIENT_KEY

default: *idcfcloud
```

# 使い方

## クラウドへの接続

pryを起動すると```fog```というコマンドが使えるようになっています。

次のように引数にプロファイル名を与えると、そのプロファイルを使ってクラウドに接続し、
新しいpryのセッションが開始されます。プロファイル名を与えなかった場合には```default``` が使用されます。

```
[1] pry(main)> fog idcfcloud
[1] fog[compute:cloudstack:idcfcloud]> 
```

スコープがfogの接続オブジェクトになっているので、各種メソッドを実行することができます。

```
[1] fog[compute:cloudstack:idcfcloud]> zones
```

```exit``` コマンドで現在のセッションを抜けることができます。

```
[2] fog[compute:cloudstack:idcfcloud]> exit
```

## プロバイダの指定

```provider``` を指定していなかったり、複数のプロバイダの接続情報をまとめて書いている場合にはオプション```-p``` でプロバイダを指定することができます。

```
[1] pry(main)> fog -p cloudstack default
```

## 戻り値

fogコマンドは```_``` で接続したfogのオブジェクトを返します。
fogのオブジェクトのみが欲しい場合には、```-n```オプションを付けるとセッションを開始せずに```_``` だけを返します。

```
[1] pry(main)> fog -n idcfcloud
[2] pry(main)> c = _
[3] pry(main)> c.zones
```

# 使用例

以下ではプロンプトを「>>」で表します。出力は適当に省略しています。

## IDCFクラウドで仮想マシン作成・削除

まずIDCFクラウドに接続します。

```irb
>> fog idcfcloud
```

仮想マシンを作成するために必要なゾーン、テンプレート、サービスオファリングの情報を取得します。

ゾーンの情報は```zones``` で取得できます。現在は1ゾーンしかないので最初の要素を取得します。

```irb
>> zone = zones.first;
```

テンプレートは```images``` で取得できます。imagesは内部的に```listTemplates``` を ```templatefitler=self``` で呼んでいるので、提供されているテンプレートも取得するにはオプションで```templatefilter``` の値を指定する必要があります。

```irb
>> images
=> []
>> images.all('templatefilter'=>'executable').collect { |i| i.name }
=> ["CentOS 7.0 64-bit",
 "CentOS 6.5 64-bit",
 "CentOS 5.10 64-bit",
 "CentOS 5.10 32-bit",
 "Red Hat Enterprise Linux 6.5 64-bit",
 "Red Hat Enterprise Linux 5.10 64-bit",
 "Red Hat Enterprise Linux 5.10 32-bit",
 "Ubuntu Server 14.04 LTS 64-bit",
 "Ubuntu Server 12.04 LTS 64-bit",
 "Ubuntu Server 10.04 LTS 64-bit",
 "VyOS 1.0.4 64-bit",
 "AppTemplate CentOS 6.5 64-bit",
 "CentOS 6.5 64-bit for Vagrant",
 "ubuntu-1gb-opt",
 "Windows Server 2008 R2 Ent SP1",
 "Windows Server 2012 R2 Std",
 "cl-base-centos65-08192014",
 "Windows Server 2012 Std",
 "Windows Server 2012 R2 Std + SQL2014 Std",
 "Windows Server 2012 R2 Std + SQL2012 Std SP2",
 "Ubuntu-1G",
 "Windows Server 2012 R2 Std + SQL2008 R2 Std SP3",
 "Windows Server 2012 Std + SQL2014 Std",
 "Ubuntu-1GB-2",
 "Windows Server 2012 Std + SQL2008 R2 Std SP3",
 "Windows Server 2008 R2 Ent SP1 + SQL2014 Std",
 "Windows Server 2008 R2 Ent SP1 + SQL2012 Std SP2",
 "Windows Server 2008 R2 Ent SP1 + SQL2008 R2 Std SP3",
 "Ubuntu-1GB-op",
 "CoreOS (stable) 494.4.0 64-bit",
 "RDB Service BASE Template",
 "Windows Server 2012 Std + SQL2012 Std SP2"]
```

今回は```CentOS 7.0 64-bit``` を使用します。findメソッドを使って特定の要素を抜き出すことができます。

```irb
>> image = images.all('templatefilter'=>'executable').find{|i| /CentOS 7.0 64-bit/ =~ i.name }
```

サービスオファリングは```flavors``` で所得できます。getメソッドでUUIDを指定して```light.S1``` を指定します。

```irb
>> flavors.collect{|f| [f.id, f.name, f.cpu_number, f.cpu_speed, f.memory] }
=> [["e01a9f32-55c4-4c0d-9b7c-d49a3ccfd3f6", "light.S1", 1, 800, 1024],
 ["d1aac6d2-bb47-4106-90d0-6a73ac3ae78e", "light.S2", 1, 800, 2048],
 ["55621f17-4d38-457c-ba34-e6199701b67b", "standard.S4", 1, 2400, 4096],
 ["7ae143a6-5662-4f1d-bc4c-10defa775bcb", "standard.M8", 2, 2400, 8192],
 ["6a99ff4c-1a24-4aa6-b4cc-600220987ed0", "standard.L16", 4, 2400, 16384],
 ["354c62e6-b99b-42f2-b5c7-e741f1085422", "standard.XL32", 8, 2400, 32768],
 ["8cf15770-c3c8-4efc-8ae5-b8327790db76", "highcpu.M4", 2, 2600, 4096],
 ["435c1aab-e796-42c7-9320-22ebdc8f50aa", "highcpu.L8", 4, 2600, 8192],
 ["6fda5e0c-e64d-46ea-893d-7e2ac9e128e7", "highcpu.XL16", 8, 2600, 16384],
 ["7c548831-427b-437c-9c8b-80dde8031303", "highcpu.2XL32", 16, 2600, 32768],
 ["12e39b73-3ce6-4e57-9036-3dac0c2b2b06", "highmem.M16", 2, 2200, 16384],
 ["d59817bc-ed79-4083-8b71-51b26c76d311", "highmem.L32", 4, 2200, 32768],
 ["ee5ee568-76b2-46ad-9221-c695e6f2149d", "highmem.XL64", 8, 2200, 65536],
 ["9a2f3ee4-af46-4790-9331-753674c16e68", "highio.5XL128", 40, 2492, 131072]]
>> flavor = flavors.get("e01a9f32-55c4-4c0d-9b7c-d49a3ccfd3f6")
```

仮想マシン作成に必要な情報が集まったので、いよいよ仮想マシンを作成します。

```irb
>> server = servers.create(:display_name=>"fogtest", :zone_id=>zone.id, :image_id=>image.id, :flavor_id=>flavor.id)
```

```servers.create``` を実行すると作成中のServerのインスタンスが返ります。
作成が完了するのを待つには、次のように```wait_for``` メソッドを使用します。

```irb
>> server.wait_for { ready? }
```

仮想マシンを削除する際には```destroy``` メソッドを使用します。

```irb
>> server.destroy
```

## DigitalOceanで仮想マシン作成・削除

まずは、DigitalOceanに接続します。

```irb
>> fog digitalocean
```

ほとんどの操作はIDCFクラウドと同じですが、DigitalOceanの場合にはzonesではなくregionsを使用します。
また、作成した仮想マシンにSSHで接続するため作成時に```ssh_key_ids``` を指定しています。

```irb
>> region = regions.first
>> images.all.collect{|i| [i.id, i.distribution, i.name] }
=> [[9836782, "CoreOS", "557.0.0 (alpha)"],
 [9836871, "CoreOS", "522.4.0 (beta)"],
 [9836874, "CoreOS", "522.4.0 (stable)"],
 [6370882, "Fedora", "20 x64"],
 [6370885, "Fedora", "20 x32"],
 [6370968, "Fedora", "19 x64"],
 [6370969, "Fedora", "19 x32"],
 [6372105, "CentOS", "6.5 x32"],
 [6372108, "CentOS", "6.5 x64"],
 [6372321, "CentOS", "5.10 x64"],
 [6372425, "CentOS", "5.10 x32"],
 [6372526, "Debian", "7.0 x64"],
 [6372528, "Debian", "7.0 x32"],
 [6372581, "Debian", "6.0 x64"],
 [6372662, "Debian", "6.0 x32"],
 [6374124, "Ubuntu", "10.04 x64"],
 [6374125, "Ubuntu", "10.04 x32"],
 [6374128, "Ubuntu", "12.04.5 x64"],
 [6374130, "Ubuntu", "12.04.5 x32"],
 [7053293, "CentOS", "7.0 x64"],
 [9801948, "Ubuntu", "14.04 x32"],
 [9801950, "Ubuntu", "14.04 x64"],
 [9801951, "Ubuntu", "14.10 x32"],
 [9801954, "Ubuntu", "14.10 x64"],
 [6376601, "Ubuntu", "Ruby on Rails on 14.04 (Nginx + Unicorn)"],
 [6423475, "Ubuntu", "WordPress on 14.04"],
 [6732690, "Ubuntu", "LEMP on 14.04"],
 [6732691, "Ubuntu", "LAMP on 14.04"],
 [6734697, "Ubuntu", "Django on 14.04"],
 [6798184, "Ubuntu", "MEAN on 14.04"],
 [8375425, "Ubuntu", "Drupal 7.34 on 14.04"],
 [8383942, "Ubuntu", "Dokku v0.2.3 on 14.04 (w/ Docker 1.3.2)"],
 [8412876, "Ubuntu", "Magento 1.9.1.0 on 14.04"],
 [8917824, "Ubuntu", "ownCloud 7.0.4 on 14.04"],
 [9063170, "Ubuntu", "Ghost 0.5.7 on 14.04"],
 [9303378, "Ubuntu", "node-v0.10.34 on 14.04"],
 [9435356, "Ubuntu", "Docker 1.4.1 on 14.04"],
 [9723053, "Ubuntu", "GitLab 7.6.2 CE on 14.04"],
 [7572830, "Ubuntu", "Redmine on 14.04"]]
>> image = images.get(6372108)
>> flavor = flavors.all.find{|f| /512MB/ =~ f.name}
>> ssh_key = ssh_keys.first
>> server = servers.create(:name=>"fogtest", :region_id=>region.id, :image_id=>image.id, :flavor_id=>flavor.id, :ssh_key_ids=>[ssh_key.id])
>> server.wait_for { ready? }
>> server.ssh("hostname").first.stdout
=> "fogtest\r\n"
>> server.destroy
```
## S3でバケット作成・ファイル転送

fogでは仮想マシンの操作だけでなく、オブジェクトストレージを扱うこともできます。

例としてS3の操作をしてみます。

オブジェクトストレージを扱う際にはオプションで```-r storage``` を指定して接続します。

```irb
>> fog -r storage aws
```

バケットの一覧は```directories``` で見ることができます。

```irb
>> directories
=> []
```

テスト用にバケットを作成します。

```irb
>> directories.create(:key=>"fogtest#{Time.now.to_i}")
=>   <Fog::Storage::AWS::Directory
    key="fogtest1420954226",
    creation_date=nil,
    location="us-east-1"
  >
```

バケットにファイルを作成します。

```irb
>> directories.first.files.create(:key=>"test.txt", :body=>"test")
```

```public``` を```true``` にすることでファイルを公開することができます。

```irb
>> file.public = true
>> file.save
>> Net::HTTP.get(URI.parse(file.public_url))
=> "test"
```

```body``` に値をセットすることで、ファイルの中身を書き換えることができます。

```irb
>> file.body = "test2"
>> file.save
```

これを利用するとローカルのファイルをS3に送ることができます。

```irb
>> file.body = File.open("/path/to/file")
>> file.save
```

もちろん作成時に指定してもOKです。

```irb
>> directories.first.files.create(:key=>"test.txt", :body=>File.open("/path/to/file"))
```

逆にS3上のファイルを持ってくる際にはローカルでファイルを作成しbodyの中身を書き込みます。

```irb
>> File.write(file.key, file.body)
```

```destroy``` メソッドでファイル・ディレクトリを削除することができます。

```irb
>> file.destroy
>> directories.first.destroy
```

# 最後に

Vagrantのプラグインを作成するためにfogを使う必要があったので、pryから使ってみたところ想像以上に便利でした。pryからライブラリを使うと便利な場面は他にもたくさんありそうです。カスタムコマンドの作成についてはpryのWikiにも情報がありますがpryの[コマンドのコード](https://github.com/pry/pry/tree/master/lib/pry/commands)も参考になると思います。