serverspec-runnerにて独自リソースを使う

![foa6fq.png](https://qiita-image-store.s3.amazonaws.com/0/14093/163bc897-64ea-3877-675b-0002dfa0493d.png)

# serverspec-runnerとは？
インフラの状態をテストするフレームワーク[serverspec](http://serverspec.org/)を複数のホストに対し気軽に使えて、わかりやすい結果を表示するツールです。
詳しくは↓こちら

#### [serverspec-runnerを使って複数のホストのテストレポートを作る](http://qiita.com/y05_net/items/1ebc7b82f1a6ffa69ed0)

#### [serverspec-runner@github.com](https://github.com/hiracy/serverspec-runner)

# 独自リソースを使う理由
serverspec-runnerは[serverspec.orgのリソース](http://serverspec.org/resource_types.html)に準拠しています。
serverspecを使い込んでくると、どうしても組み込みのリソースでは足りず、その場合はcommandリソースによるコマンドラインやシェルスクリプトの実行になってしまい、コードの再利用性や可読性が下がってしまうため、独自のリソースを使った方が良いケースが出てくるかと思います。

# serverspecのリソース拡張について
serverspecの拡張方法は作者のmizzy氏自ら[O'Reilly Japanの「Serverspec」](http://www.oreilly.co.jp/books/9784873117096/)にて1章を使って丁寧に詳しく書かれております。
serverspec-runnerでリソース拡張する際もここで書かれている内容の通りコードを記述します。
尚、この技術書にて詳細が書かれておりますので**serverspec自体の拡張に関してはここではあまり触れません**。ご興味がある方は一読することをお勧めします！

# 事前準備
rubyが使用できる環境にてserverspec-runnerをインストールし、テストコードのスケルトンを生成します。

```
gem install serverspec-runner
```
テストで使用するディレクトリ/tmp/testroot を指定します。
以降「**spec_root**」と呼ぶ事にします。
```
serverspec-runner -r /tmp/testroot
want to create spec-tree to /tmp/testroot? (y/n): y
```

```
cd /tmp/testroot
```

# spec_rootディレクトリ
lsコマンドで見ると以下のようなファイル・ディレクトリが生成されています。

 - scenario.yml
    - [実行時シナリオ](http://qiita.com/y05_net/items/1ebc7b82f1a6ffa69ed0#scenarioyml%E3%81%AE%E4%BD%9C%E3%82%8A%E6%96%B9)
 - ssh_options_default.yml
    - [実行時のnet-sshデフォルトオプション](http://net-ssh.github.io/ssh/v2/api/classes/Net/SSH/Config.html)
 - spec
    - serverspecテストコードの場所
 - lib
    - **serverspec-runner拡張リソースコードの場所**

# サンプル1: 既存リソースを拡張
実用性やテスト内容の妥当性はとりあえず置いといて、**ファイルがテキストファイルかを判別するリソース拡張**をしてみます。

```$spec_root/lib/extension/serverspec/type/file.rb
module Serverspec::Type
  class File < Base
    def text?
      @runner.check_file_is_text(@name)
    end
  end
end
```

テキストファイル判別はfileコマンドの結果をgrepすることにしてみます。

```$spec_root/lib/extension/specinfra/command/base/file.rb
class Specinfra::Command::Base::File < Specinfra::Command::Base
  class << self
    def check_is_text(file)
      "file #{escape(file)} | egrep ' text$'"
    end
  end
end
```

拡張部分は以上です。
今度はテストコードのほうから、本家のfileリソースには無い**be_text**を使用してみます。

```$spec_root/spec/example/default.rb
require 'spec_helper'

describe file('/tmp/anyfile') do
  it { should be_text }
end
```

テストを成功させるために/tmp/anyfile を作ります。

```
echo 'あいうえお' > /tmp/anyfile
```

それでは結果を試してみましょう。
尚、今回は生成時デフォルトのscenario.ymlによるローカルホストのテストですが、リモートホストに実行するには[こちら](http://qiita.com/y05_net/items/1ebc7b82f1a6ffa69ed0#scenarioyml%E3%81%AE%E4%BD%9C%E3%82%8A%E6%96%B9)を参照して下さい。

```
serverspec-runner
```

```出力結果
+---------------------------------------+
|description                   | result |
+---------------------------------------+
|example@anyhost-01(127.0.0.1) |        |
|  File "/tmp/anyfile"         |        |
|                              |   OK   |
+---------------------------------------+
```

今度は失敗するケースを試してみます。

```
dd if=/dev/zero of=/tmp/anyfile count=10 bs=1
serverspec-runner
```

```出力結果
+---------------------------------------+
|description                   | result |
+---------------------------------------+
|example@anyhost-01(127.0.0.1) |        |
|  File "/tmp/anyfile"         |        |
|                              |   NG   |
+---------------------------------------+
```

# サンプル2: 新たにリソースを定義
今度は**mysql**という全く新しいリソースを定義し、レプリケーションが正常にできているかチェックできるようにしてみます。構文は以下のようにしようと思います。

```
be_replicated.from(<レプリケーションマスターサーバのIPアドレス>)
  .with_user(<mysqlログイン用ユーザ>)
  .with_password(<mysql用ログインパスワード>)
  .with_port(<mysqlログイン用ポート>)
```

Serverspec::Type及びSpecinfra::Command クラスを定義します。

```$spec_root/lib/extension/serverspec/type/mysql.rb
module Serverspec::Type
  class Mysql < Base
    def replicated?(master=nil, user=nil, password=nil, port=nil)
      @runner.check_mysql_is_replicated(master, user, password, port)
    end
  end
end
```

```$spec_root/lib/extension/specinfra/command/linux/base/mysql.rb
class Specinfra::Command::Linux::Base::Mysql < Specinfra::Command::Base
  class << self
    def check_is_replicated(master=nil, user=nil, password=nil, port=nil)
      opt_user     = "--user=#{user} " || ''
      opt_password = "--password=#{password} " || ''
      opt_port     = "--port=#{port} " || ''

      cmd = ''
      cmd += "echo 'show slave status \\G;' | mysql #{opt_user} #{opt_password} #{opt_port} | "
      cmd += "grep -e 'Slave_IO_Running: Yes' -e 'Slave_SQL_Running: Yes' -e 'Master_Host: #{master}' | "
      cmd += "wc -l | grep -w 3"
      cmd
    end
  end
end
```

be_replicatedという新しい構文はRSpecのマッチャに処理されないので、自前で定義します。

```$spec_root/lib/extension/serverspec/matcher/be_replicated.rb
RSpec::Matchers.define :be_replicated do
  match do |host|
    host.replicated?(@master, @user, @password, @port)
  end

  chain :from do |master|
    @master = master
  end

  chain :with_user do |user|
    @user = user
  end

  chain :with_password  do |password|
    @password = password
  end

  chain :with_port do |port|
    @port = port
  end
end
```

最後に、serverspecのフレームワークが[Serverspec::Helper::Type](https://github.com/serverspec/serverspec/blob/master/lib/serverspec/helper/type.rb)にてやっていることと同じように、新しいリソースタイプを認識させるようにします。

```$spec_root/lib/extension/serverspec/helper/type.rb
module Serverspec
  module Helper
    module Type
      types = %w(
        mysql
      )

      types.each {|type| require "extension/serverspec/type/#{type}" }

      types.each do |type|
        define_method type do |*args|
          name = args.first
          eval "Serverspec::Type::#{type.to_camel_case}.new(name)"
        end
      end
    end
  end
end
```

以上が拡張部分です。次にテストコードを書きます。

```$spec_root/spec/example/default.rb
require 'spec_helper'

describe mysql("replication test") do
  it { should be_replicated.from('192.168.10.10')
         .with_user('repluser')
         .with_password('replpasswd')
         .with_port('3306') }
end
```

それでは試してみましょう。
(予め下記ホスト間にてリプリケーションは正常にされているものとします)

```
serverspec-runner
```

```出力結果
+---------------------------------------+
|description                   | result |
+---------------------------------------+
|example@anyhost-01(127.0.0.1) |        |
|  Mysql "replication test"    |        |
|                              |   OK   |
+---------------------------------------+
```

# 最後に
[O'Reilly Japanの「Serverspec」](http://www.oreilly.co.jp/books/9784873117096/)と[github.comのserverspec/specinfraページ](https://github.com/serverspec)を見てserverspec-runner側からリソースを拡張する方法を実装してみました。
serverspecそのものの拡張に対しての間違いやもっとスマートな実装がありましたらコメントにでもこっそり教えて頂けると嬉しいです。