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


serverspec-runnerとは?

インフラの状態をテストするフレームワークserverspecを複数のホストに対し気軽に使えて、わかりやすい結果を表示するツールです。
詳しくは↓こちら

serverspec-runnerを使って複数のホストのテストレポートを作る

[email protected]

独自リソースを使う理由

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

serverspecのリソース拡張について

serverspecの拡張方法は作者のmizzy氏自らO'Reilly Japanの「Serverspec」にて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コマンドで見ると以下のようなファイル・ディレクトリが生成されています。

サンプル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によるローカルホストのテストですが、リモートホストに実行するにはこちらを参照して下さい。

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にてやっていることと同じように、新しいリソースタイプを認識させるようにします。

$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」github.comのserverspec/specinfraページを見てserverspec-runner側からリソースを拡張する方法を実装してみました。
serverspecそのものの拡張に対しての間違いやもっとスマートな実装がありましたらコメントにでもこっそり教えて頂けると嬉しいです。