Serverspecでロジックとテスト内容を分離してテスト内容をYAMLで管理するようにしてみた。

# はじめに

前に書いた記事[Serverspecでロジックとテストケースの分離を検討してみた](http://qiita.com/h-ymmr/items/ae8bcde10c7613e76c63)の続きです。

# テストケース

YAMLファイルにチェックしたい内容を記載します。
今回はapacheを例に取り上げました。

```yaml:$TEST_HOME\sample\env\apache_conf.yml
# チェック内容
CONF:
  # ファイル内容の確認
  FILE:
    httpd.conf:
      PATH: C:\APP\Apache\etc\httpd\conf\httpd.conf
      STATUS: true
      MATCH:
        Server:
          - ServerRoot "C:/APP/Apache/etc/httpd"
          - ServerTokens ProductOnly
          - ServerSignature Off
        PidFile:
          - PidFile run/httpd.pid
        Timeout:
          - Timeout 120
        KeepAlive:
          - KeepAlive Off
        Include:
          - Include conf.d/*.conf
        LogFormat:
          - LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\" %D" combined_time
        LoadModule:
          - LoadModule authz_host_module modules/mod_authz_host.so
          - LoadModule log_config_module modules/mod_log_config.so
          - LoadModule deflate_module modules/mod_deflate.so
          - LoadModule headers_module modules/mod_headers.so
      UNMATCH:
        LoadModule:
          - LoadModule auth_digest_module modules/mod_auth_digest.so
          - LoadModule authn_alias_module modules/mod_authn_alias.so
  # レジストリ登録内容の確認
  REGISTRY:
    service:
      PATH: HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\services\Apache2.2
      STATUS: true
      MATCH:
        type_string:
          DisplayName: Apache2.2
        type_dword:
          Type: 16
        type_expandstring:
          ImagePath: \"C:\APP\Apache\bin\httpd.exe\" -k runservice
# ホスト毎のチェック項目マッピング
STG-WEB01:
  FILE:
    - httpd.conf
  REGISTRY:
    - service
STG-WEB02:
  FILE:
    - httpd.conf
  REGISTRY:
    - service
```

# ファイル内容をチェックするspecファイル

specファイルからyamlファイルを読み込み、現在のサーバのテスト内容を判別して評価します。

```ruby:$TEST_HOME\sample\spec\apache\apache_conf_spec.rb
require 'spec_helper'
require 'yaml'
require 'CmnClsHelper'

objHelper = CmnClsHelper.new
this_file_name = File.basename( __FILE__, ".rb" )
this_file_dir_path = File.dirname( File.expand_path( __FILE__ ) )
this_parent_name = File.basename( this_file_dir_path )
target_host      = "#{ENV['TARGET_HOST']}".upcase
this_yaml_name   = this_file_name.gsub("_spec", "")
path_f_yml       = "env/#{this_yaml_name}.yml"
strHR            = "=" * 60
path_d_parser    = "C:\\Tool\\Infra\\serverspec\\parser"
path_d_cmd       = "C:\\Tool\\Infra\\serverspec\\cmd"

puts "PATH_F_YAML = #{path_f_yml}"
h_all   = YAML.load_file(path_f_yml)

if h_all.key?(target_host) && h_all["CONF"] && h_all["CONF"].instance_of?( Hash )
  check_file_list   = []
  h_replace_list    = {}
  target_category   = "FILE"
  if h_all["#{target_host}"].instance_of?( Hash ) && h_all["#{target_host}"]["#{target_category}"]
    if h_all["#{target_host}"]["#{target_category}"].instance_of?( Array )
      h_all["#{target_host}"]["#{target_category}"].each do |filename|
        check_file_list   << filename
      end
    end
  end
  if h_all["#{target_host}"].instance_of?( Hash ) && h_all["#{target_host}"]["REPLACE"]
    if h_all["#{target_host}"]["REPLACE"].instance_of?( Hash )
      h_all["#{target_host}"]["REPLACE"].each do |target_var_name, h_val|
        h_replace_list["#{target_var_name}"]  = {}  if ! h_replace_list.key?("#{target_var_name}")
        h_val.each do |replace_key, replace_val|
          h_replace_list["#{target_var_name}"]["#{replace_key}"]  = "#{replace_val}"
        end
      end
    end
  end
  if h_all["CONF"]["#{target_category}"] && h_all["CONF"]["#{target_category}"].instance_of?( Hash )
    check_file_list.each do |target_name|
      context "#{strHR}\n[#{target_host}] #{this_parent_name}/#{this_file_name} : #{target_name}\n#{strHR}\n" do
        if h_all["CONF"]["#{target_category}"]["#{target_name}"] && h_all["CONF"]["#{target_category}"]["#{target_name}"].instance_of?( Hash )
          h_cur_test_case   = h_all["CONF"]["#{target_category}"]["#{target_name}"]
          ary_ignore_spaces = []
          h_check_list    = {}
          blnIsPipeParse    = false
          cmd_parser      = ""
          path_f_taget  = "NOT_DEFINED"
          path_f_taget  = h_cur_test_case["PATH"]               if h_cur_test_case["PATH"]
          if h_replace_list["PATH"] && h_replace_list["PATH"].instance_of?( Hash )
            h_replace_list["PATH"].each do |replace_key, replace_val|
              path_f_taget  = path_f_taget.gsub("#{replace_key}","#{replace_val}")
            end
          end
          if h_cur_test_case.key?("PIPE-PARSE") && objHelper.isNotNull(h_cur_test_case["PIPE-PARSE"])
            blnIsPipeParse    = true
            case File.extname(h_cur_test_case["PIPE-PARSE"])
              when ".rb" then
                cmd_parser  = "ruby #{path_d_parser}\\#{h_cur_test_case['PIPE-PARSE'].strip}"
              else
                cmd_parser  = "#{path_d_parser}\\#{h_cur_test_case['PIPE-PARSE'].strip}"
            end
          end
          if h_cur_test_case.key?("STATUS")
            stat_f_taget  = h_cur_test_case["STATUS"]
            describe file("#{path_f_taget}") do
              if stat_f_taget
                it { should be_file }
              else
                it { should_not be_file }
              end
            end
          end
          if h_cur_test_case.key?("IGNORE_MULTI_SPACES") && h_cur_test_case["IGNORE_MULTI_SPACES"].instance_of?( Array )
            ary_ignore_spaces = h_cur_test_case["IGNORE_MULTI_SPACES"]
          end
          ["MATCH", "UNMATCH","MATCH-REGEX", "UNMATCH-REGEX", "FILE-MATCH", "FILE-UNMATCH", "FILE-MATCH-REGEX", "FILE-UNMATCH-REGEX"].each do |check_action|
            blnIsCmd    = true
            blnIsMatch    = true
            blnIsUseRegex = false
            action      = "MATCH"
            if h_cur_test_case[check_action] && h_cur_test_case[check_action].instance_of?( Hash )
              case "#{check_action}"
                when "FILE-MATCH"
                  blnIsCmd    = false
                when "FILE-MATCH-REGEX"
                  blnIsCmd    = false
                  blnIsUseRegex = true
                when "FILE-UNMATCH"
                  blnIsCmd    = false
                  blnIsMatch    = false
                  action      = "UNMATCH"
                when "FILE-UNMATCH-REGEX"
                  blnIsCmd    = false
                  blnIsMatch    = false
                  blnIsUseRegex = true
                  action      = "UNMATCH"
                when "MATCH-REGEX"
                  blnIsUseRegex = true
                when "UNMATCH-REGEX"
                  blnIsMatch    = false
                  blnIsUseRegex = true
                  action      = "UNMATCH"
                when "UNMATCH"
                  blnIsMatch    = false
                  action      = "UNMATCH"
              end
              h_cur_test_case[check_action].each do |filter_name, ary_case|
                # 器の用意
                h_check_list["#{filter_name}"]        ||= {}
                h_check_list["#{filter_name}"]["MATCH"]   ||= []
                h_check_list["#{filter_name}"]["UNMATCH"] ||= []
                h_check_list["#{filter_name}"]["CMD"]   = blnIsCmd
                # 
                ary_case.each do |test_case|
                  blnIsIgnoreSpaces = ary_ignore_spaces.include?(filter_name) ? true : false
                  tmp         = objHelper.convCheckStr({"str" => "#{test_case}".strip, "blnIsUseRegex" => blnIsUseRegex, "blnIsIgnoreSpaces" => blnIsIgnoreSpaces})
                  tmp         = "#{tmp}".encode("Windows-31J")
                  h_check_list["#{filter_name}"]["#{action}"]   << "#{tmp}"   if "#{tmp}" != ""
                end
              end
            end
          end
          h_check_list.each do |filter_name, h_case|
            if h_case["CMD"]
              # 「|」で日本語が化けるため、バッチを用意（バッチ内の「|」だと日本語が化けない）
              exec_cmd        = "#{path_d_cmd}\\cat.cmd #{path_f_taget}"
              if blnIsPipeParse
                if "#{filter_name}".upcase != "NO_GREP"
                  exec_cmd    = "#{path_d_cmd}\\cat_parser_grep.cmd #{path_f_taget} #{filter_name} #{cmd_parser}"
                else
                  exec_cmd    = "#{path_d_cmd}\\cat_parser.cmd #{path_f_taget} #{cmd_parser}"
                end
              else
                if "#{filter_name}".upcase != "NO_GREP"
                  exec_cmd    = "#{path_d_cmd}\\cat_grep.cmd #{path_f_taget} #{filter_name}"
                end
              end
              # ファイル内容出力コマンド
              describe command("#{exec_cmd}") do
                h_case["MATCH"].each do |test_case|
                  its(:stdout) { should match /#{test_case}/ }
                end
                h_case["UNMATCH"].each do |test_case|
                  its(:stdout) { should_not match /#{test_case}/ }
                end
              end
            else
              describe file("#{path_f_taget}") do
                h_case["MATCH"].each do |test_case|
                  its(:content) { should match /#{test_case}/ }
                end
                h_case["UNMATCH"].each do |test_case|
                  its(:content) { should_not match /#{test_case}/ }
                end
              end
            end
          end
        end
      end
    end
  end
end
```

# レジストリ設定内容をチェックするspecファイル

specファイルからyamlファイルを読み込み、現在のサーバのテスト内容を判別して評価します。

```ruby:$TEST_HOME\sample\spec\apache\apache_registry_spec.rb
require 'spec_helper'
require 'yaml'
require 'CmnClsHelper'

objHelper = CmnClsHelper.new
this_file_name = File.basename( __FILE__, ".rb" )
this_file_dir_path = File.dirname( File.expand_path( __FILE__ ) )
this_parent_name = File.basename( this_file_dir_path )
target_host = "#{ENV['TARGET_HOST']}".upcase
path_f_yml = "env/apache_conf.yml"
strHR = "=" * 60
h_all = YAML.load_file(path_f_yml)
if h_all.key?(target_host) && h_all["CONF"] && h_all["CONF"].instance_of?( Hash )
  check_target_list = []
  target_category   = "REGISTRY"
  if h_all["#{target_host}"].instance_of?( Hash ) && h_all["#{target_host}"]["#{target_category}"]
    if h_all["#{target_host}"]["#{target_category}"].instance_of?( Array )
      h_all["#{target_host}"]["#{target_category}"].each do |target_name|
        check_target_list   << target_name
      end
    end
  end
  if h_all["CONF"]["#{target_category}"] && h_all["CONF"]["#{target_category}"].instance_of?( Hash )
    check_target_list.each do |target_name|
      context "#{strHR}\n[#{target_host}] #{this_parent_name}/#{this_file_name} : #{target_name}\n#{strHR}\n" do
        if h_all["CONF"]["#{target_category}"]["#{target_name}"] && h_all["CONF"]["#{target_category}"]["#{target_name}"].instance_of?( Hash )
          h_cur_test_case = h_all["CONF"]["#{target_category}"]["#{target_name}"]
          path_f_taget  = "NOT_DEFINED"
          path_f_taget  = h_cur_test_case["PATH"]               if h_cur_test_case["PATH"]
          if h_cur_test_case.key?("STATUS")
            stat_f_taget  = h_cur_test_case["STATUS"]
            describe windows_registry_key("#{path_f_taget}") do
              if stat_f_taget
                it { should exist }
              else
                it { should_not exist }
              end
            end
          end
          ["MATCH", "UNMATCH"].each do |check_action|
            if h_cur_test_case[check_action] && h_cur_test_case[check_action].instance_of?( Hash )
              blnIsMatch  = true
              blnIsMatch  = false     if check_action == "UNMATCH"
              describe windows_registry_key("#{path_f_taget}") do
                h_cur_test_case[check_action].each do |reg_type, h_list|
                  h_list.each do |key,val|
                    case reg_type
                      when "type_dword"
                      else
                        val = "#{val}".gsub("\\\"","_DQ_").gsub("_DQ_","\"")
                    end
                    case reg_type
                      when "type_string"
                        if blnIsMatch
                          it { should have_property("#{key}", :type_string) }
                          it { should have_property_value("#{key}", :type_string, "#{val}") }
                        else
                          it { should_not have_property("#{key}", :type_string) }
                          it { should_not have_property_value("#{key}", :type_string, "#{val}") }
                        end
                      when "type_dword"
                        if blnIsMatch
                          it { should have_property("#{key}", :type_dword) }
                          it { should have_property_value("#{key}", :type_dword, "#{val.to_s(16)}") }
                        else
                          it { should_not have_property("#{key}", :type_dword) }
                          it { should_not have_property_value("#{key}", :type_dword, "#{val.to_s(16)}") }
                        end
                      when "type_binary"
                        if blnIsMatch
                          it { should have_property("#{key}", :type_binary) }
                          it { should have_property_value("#{key}", :type_binary, "#{val}") }
                        else
                          it { should_not have_property("#{key}", :type_binary) }
                          it { should_not have_property_value("#{key}", :type_binary, "#{val}") }
                        end
                      when "type_qword"
                        if blnIsMatch
                          it { should have_property("#{key}", :type_qword) }
                          it { should have_property_value("#{key}", :type_qword, "#{val}") }
                        else
                          it { should_not have_property("#{key}", :type_qword) }
                          it { should_not have_property_value("#{key}", :type_qword, "#{val}") }
                        end
                      when "type_multistring"
                        if blnIsMatch
                          it { should have_property("#{key}", :type_multistring) }
                          it { should have_property_value("#{key}", :type_multistring, "#{val}") }
                        else
                          it { should_not have_property("#{key}", :type_multistring) }
                          it { should_not have_property_value("#{key}", :type_multistring, "#{val}") }
                        end
                      when "type_expandstring"
                        if blnIsMatch
                          it { should have_property("#{key}", :type_expandstring) }
                          it { should have_property_value("#{key}", :type_expandstring, "#{val}") }
                        else
                          it { should_not have_property("#{key}", :type_expandstring) }
                          it { should_not have_property_value("#{key}", :type_expandstring, "#{val}") }
                        end
                    end
                  end
                end
              end
            end
          end
        end
      end
    end
  end
end
```

# 実行

実行してみます。

```serverspec
PS C:\serverspec\sample> rake

##################################################
HOST: REMOTEHOST : development
##################################################
PATH_F_YAML = env/apache_conf.yml

============================================================
[REMOTESERVER] apache/apache_conf_spec : httpd.conf
============================================================
  File "C:\APP\Apache\etc\httpd\conf\httpd.conf"
    should be file
  Command "C:\Tool\Infra\serverspec\cmd\cat_grep.cmd C:\APP\Apache\etc\httpd\conf\httpd.conf Server"
    stdout
      should match /ServerRoot \"C:\/APP\/Apache\/etc\/httpd\"/
    stdout
      should match /ServerTokens ProductOnly/
    stdout
      should match /ServerSignature Off/
  Command "C:\Tool\Infra\serverspec\cmd\cat_grep.cmd C:\APP\Apache\etc\httpd\conf\httpd.conf PidFile"
    stdout
      should match /PidFile run\/httpd\.pid/
  Command "C:\Tool\Infra\serverspec\cmd\cat_grep.cmd C:\APP\Apache\etc\httpd\conf\httpd.conf Timeout"
    stdout
      should match /Timeout 120/
  Command "C:\Tool\Infra\serverspec\cmd\cat_grep.cmd C:\APP\Apache\etc\httpd\conf\httpd.conf KeepAlive"
    stdout
      should match /KeepAlive Off/
  Command "C:\Tool\Infra\serverspec\cmd\cat_grep.cmd C:\APP\Apache\etc\httpd\conf\httpd.conf Include"
    stdout
      should match /Include conf\.d\/\*\.conf/
  Command "C:\Tool\Infra\serverspec\cmd\cat_grep.cmd C:\APP\Apache\etc\httpd\conf\httpd.conf LogFormat"
    stdout
      should match /LogFormat \"%h %l %u %t \\\"%r\\\" %>s %b \\\"%\{Referer\}i\\\" \\\"%\{User\-Agent\}i\\\" %D\" combined_time/
  Command "C:\Tool\Infra\serverspec\cmd\cat_grep.cmd C:\APP\Apache\etc\httpd\conf\httpd.conf LoadModule"
    stdout
      should match /LoadModule authz_host_module modules\/mod_authz_host\.so/
    stdout
      should match /LoadModule log_config_module modules\/mod_log_config\.so/
    stdout
      should match /LoadModule deflate_module modules\/mod_deflate\.so/
    stdout
      should match /LoadModule headers_module modules\/mod_headers\.so/
    stdout
      should not match /LoadModule auth_digest_module modules\/mod_auth_digest\.so/
    stdout
      should not match /LoadModule authn_alias_module modules\/mod_authn_alias\.so/

============================================================
[REMOTESERVER] apache/apache_registry_spec : service
============================================================
  Windows registry key "HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\services\Apache2.2"
    should exist
  Windows registry key "HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\services\Apache2.2"
    should have property "DisplayName", :type_string
    should have property value "DisplayName", :type_string, "Apache2.2"
    should have property "Type", :type_dword
    should have property value "Type", :type_dword, "16"
    should have property "ImagePath", :type_expandstring
    should have property value "ImagePath", :type_expandstring, "\"C:\\APP\\Apache\\bin\\httpd.exe\" -k runservice"

Finished in 49.38 seconds (files took 1.82 seconds to load)
132 examples, 0 failures ← テストケースは記事作成に際して一部削除してます。

##################################################

PS C:\serverspec\sample>
```