目的

serverspec は rspec の仕組みを使ってサーバのテストを行うフレームワークです。
serverspec では サーバのテストをするために file や group といったいくつかのリソースタイプ が定義されています。しかし、いくつかのテストサンプルを書いているとそのサンプル専用のリソースタイプが欲しくなることがあります。

たとえば、「/var がマウントポイントとして存在し、かつ、ディスク使用量が80%未満であること」をテストしたい場合、mount のような専用のリソースタイプは(まだ)用意されていないので、 command リソースを使って次のようなサンプルを記述します。

mount_spec.rb

# マウントポイントが存在するか
describe command("df | sed 's/\\s\\{1,\\}/ /g' | cut -d' ' -f6 | grep -qE '^/var$'") do
  it { should return_exit_status 0 }
end

# ディスクの使用量が80%未満か
describe command("PERCENTAGE=`df /var | tail -1 | sed 's/\\s\\{1,\\}/ /g' | cut -d' ' -f5 | sed 's/%//g'` ; test 80 -gt $PERCENTAGE") do
  it { should return_exit_status 0 }
end

command リソースは引数で受けた文字列をシェルで実行し、その結果から成否を判定します。シェルで実行できるコマンドであれば何でもテストが行えるため、柔軟なテストが可能ですが、少し複雑な処理が入るとぱっと見ただけで何のテストをしているのかわかりずらくなります。

そこで、serverspec に専用のリソースタイプを追加して(([本来であれば、serverspec をフォークし、新しいリソースタイプを追加した上で pull リクエストを送るのが筋ですが、一時的やある特定の目的のためだけのリソースはカスタムリソースで対応するのも手だと思います]))、次のような読みやすいサンプルコードを書けるようにします。

# カスタムリソースタイプを追加した後のサンプル
# ※デフォルトの serverspec では動きません
describe mount("/var") do
  it { should be_exist }                            # マウントポイントが存在するか
  its(:percentage_of_disk_used)  { should be < 80 } # ディスクの使用量が80%未満か
end

注意:対象バージョン

ここで紹介するカスタムリソースの追加は以下の環境でのみ動作を確認しています。
異なるOSや、バージョンアップ等で serverspec の構造が変わった場合は動かなくなる可能性があります。

  • Debian 7.0
  • serverspec 0.14.2

ファイル構造

ファイルの構成は任意ですが、今回は以下のような構成にしました(([serverspec-init を実行した後にカスタムリソース用のファイルを追加しただけです]))。
mount_spec.rb が serverspec のサンプルを実行する spec ファイル、mount.rb がカスタムリソースを定義するファイルです。

  .
  |-- Rakefile
  `-- spec
      |-- localhost
      |    |-- mount_spec.rb    # specファイル
      |    `-- type
      |        `-- mount.rb     # カスタムリソースタイプを定義
      `-- spec_helper.rb

カスタムリソースの追加

serverspec のリソースタイプはソースコードの ((<lib/serverspec/type|https://github.com/serverspec/serverspec/tree/master/lib/serverspec/type>)) にタイプ毎に定義されています。

定義ファイルを見ると、リソースタイプは Serverspec::Type::Base クラスのサブクラスとして定義すればよいようです。
ここでは追加するリソースタイプのクラスを Mount として以下のように定義します。

spec/localhost/type/mount.rb

# -*- coding: utf-8 -*-
module Serverspec
  module Type

    # Serverspec::Type モジュールに Mount クラスを定義
    class Mount < Base
      # _name_   :: リソースタイプのインスタンスに付ける名前(ここでは point と同じにする)
      # _point_  :: マウントポイント(例: /, /var )
      def initialize(name, point)
        @name      = name
        @point     = point
      end

      # マウントポイントが存在しているかを確認する
      # (述語マッチャなので exist? と定義しているが、spec ファイルからは
      # it { should be_exist } のように呼び出せる
      def exist?
        `df | sed 's/\\s\\{1,\\}/ /g' | cut -d' ' -f6 | grep -qE '^#{@point}$'`
        $?.exitstatus == 0 ? true : false
      end

      # マウントポイントのディスク利用率を取得する
      # rspec の attribute of subject 機能を使って its(:percentage_of_disk_used){} で
      # メソッドの結果とテストができるようになる
      def percentage_of_disk_used
        percentage = `df #{@point} | tail -1 | sed 's/\\s\\{1,\\}/ /g' | cut -d' ' -f5`
        percentage.strip.gsub("%","").to_i
      end

      # Mount インスタンスを作成するメソッド
      # describe Mount.new("/var") do ... end だけではなく
      # 他のリソースタイプと同じような describe mount("/var") do ... end のサンプルが書けるようになる
    end

    def mount(point)
      Mount.new(point, point)
    end
  end
end

# モジュールを再読み込みする
include Serverspec::Type

※Marcyさんにご指摘を頂き、 「def mount」 の位置を 「class Mount < Base」 ブロックから 「module Type」ブロックに修正しました。Marcyさんありがとうございます!

以上で、カスタムリソースの定義はおわりです。

Memo

  • シェルコマンドの実行は、 ((@specinfra@)) の SpecInfra::Command を使った方がよさそう
  • name, point が同じなら、1つにまとめてもいいかも
  • Windows などの複数の OS に対応するにはどうする?

カスタムリソースの呼び出し

追加したカスタムリソースを serverspec の スペックファイルから呼び出します。

spec/localhost/mount_spec.rb

# spec_helper を読み込む
require 'spec_helper'
# 追加したカスタムリソースのファイルを読み込む
require "localhost/type/mount"

# mount カスタムリソースを使ってサンプルを記述する
describe mount("/var") do
  it { should be_exist }
  its(:percentage_of_disk_used)  { should be < 80 }
end

serverspec の実行

スペックファイルを記述したので、プロジェクトのルートディレクトリに移動し、 serverspec を実行します。

$ rspec
Mount "/var"
  should be exist
  percentage_of_disk_used
    should be < 80

Finished in 1.7 seconds
2 examples, 0 failures

このように、目的に沿ったカスタムリソースを用意することで、最初の command リソースを使ったサンプルに比べ、どのマウントポイントに対してどのようなテストを実行したかがわかりやすくなったと思います。