AnsibleSpecでinventory_hostname等のパラメータを使う

AnsibleSpecを使ったらやりたいことといったら、ansible_*という変数をServerSpec側でも参照しながらAnsibleの結果をServerSpecで確認する、ということと思うが
普通にやってはasible_*のような暗黙的なパラメータは参照できないようになっている。

当然といえば当然だが、そうすると結局二重管理にならざるを得ず、その差を別途機械的に管理して…みたいな本末転倒の構成になる。
Googleで調べてみたら、ここにAnsibleで変数をダンプして、それをServerSpec側で読み取る方法を実現している人がいた。

この方は、HOST_VARS_PATHという値を軸に、Ansibleではダンプした変数をそこに保存し、AnsibleSpecでは変数YAMLファイルとして読み出すように、Rakefileやspec_helper.rbを修正されていた。
この方法は、実行者がすべてansible-playbookから行うという条件が満たされれば使いやすいが、Ansbleはansible-playbookで、ServerSpecはrake allで、という前提で考えている人には少々難しい(かも)。

なので、①インベントリファイルに記載されている名前で変数をダンプ ②インベントファイルに記載されている名前で変数ダンプファイルを読み出し というように書き直し、Ansibleで構築する際はansible-playbookを、構築したサーバの動作確認はrake allを実行すればいいように書き直してみた。
多分、うまくいく。

Rakefile

require 'rake'
require 'rspec/core/rake_task'
require 'yaml'
require 'ansible_spec'

properties = AnsibleSpec.get_properties
# {"name"=>"Ansible-Sample-TDD", "hosts"=>["192.168.0.103","192.168.0.103"], "user"=>"root", "roles"=>["nginx", "mariadb"]}
# {"name"=>"Ansible-Sample-TDD", "hosts"=>[{"name" => "192.168.0.103:22","uri"=>"192.168.0.103","port"=>22, "private_key"=> "~/.ssh/id_rsa"}], "user"=>"root", "roles"=>["nginx", "mariadb"]}
cfg = AnsibleSpec::AnsibleCfg.new

desc "Run serverspec to all test"
task :all => "serverspec:all"

namespace :serverspec do
  properties = properties.compact.reject{|e| e["hosts"].length == 0}
  task :all => properties.map {|v| 'serverspec:' + v["name"] }
  properties.each_with_index.map do |property, index|
    property["hosts"].each do |host|
      desc "Run serverspec for #{property["name"]}"
      RSpec::Core::RakeTask.new(property["name"].to_sym) do |t|
        puts "Run serverspec for #{property["name"]} to #{host}"
        ENV['TARGET_HOSTS'] = host["hosts"]
        ENV['TARGET_HOST'] = host["uri"]
        ENV['TARGET_PORT'] = host["port"].to_s
        ENV['TARGET_GROUP_INDEX'] = index.to_s
        ENV['TARGET_PRIVATE_KEY'] = host["private_key"]
        unless host["user"].nil?
          ENV['TARGET_USER'] = host["user"]
        else
          ENV['TARGET_USER'] = property["user"]
        end
        ENV['TARGET_PASSWORD'] = host["pass"]
        ENV['TARGET_CONNECTION'] = host["connection"]

        # 環境変数"TARGET_HOST_VARS_PATH"をセットし
        # spec/spec_helper.rbでspec_vars/{{ inventory_hostname }}.ymlを読み込ませる
        ENV['TARGET_HOST_VARS_PATH'] = sprintf("spec_vars/%s.yml", host["name"].split[0])

        roles = property["roles"]
        for role in property["roles"]
          for rolepath in cfg.roles_path
            deps = AnsibleSpec.load_dependencies(role, rolepath)
            if deps != []
              roles += deps
              break
            end
          end
        end
        t.pattern = '{' + cfg.roles_path.join(',') + '}/{' + roles.join(',') + '}/spec/*_spec.rb'
      end
    end
  end
end

タスクを生成しているループの中で、host[“name”]からインベントリファイルに記載されているホストの名前を抽出して、それを”TARGET_HOST_VARS_PATH”という環境変数に設定するよう変更しただけ。

spec/spec_helper.rb

require 'net/ssh'
require 'ansible_spec'
require 'winrm'
require 'yaml'

#
# Set ansible variables to serverspec property
#
host = ENV['TARGET_HOST']
hosts = ENV["TARGET_HOSTS"]

group_idx = ENV['TARGET_GROUP_INDEX'].to_i
vars = AnsibleSpec.get_variables(host, group_idx, hosts)
ssh_config_file = AnsibleSpec.get_ssh_config_file
if ENV['TARGET_HOST_VARS_PATH'].nil?
  set_property vars
else
  # 環境変数"TARGET_HOST_VARS_PATH"でホスト毎の変数ファイルを指定する必要がある
  host_vars = YAML.load_file(ENV['TARGET_HOST_VARS_PATH'])
  set_property host_vars.update(vars)
end

connection = ENV['TARGET_CONNECTION']

case connection
when 'ssh'
#
# OS type: UN*X
#
  set :backend, :ssh

  if ENV['ASK_BECOME_PASSWORD']
    begin
      require 'highline/import'
    rescue LoadError
      fail "highline is not available. Try installing it."
    end
    set :become_password, ask("Enter become password: ") { |q| q.echo = false }
  else
    set :become_password, ENV['BECOME_PASSWORD']
  end

  options = Net::SSH::Config.for(host)

  options[:user] = ENV['TARGET_USER'] || options[:user]
  options[:port] = ENV['TARGET_PORT'] || options[:port]
  options[:keys] = ENV['TARGET_PRIVATE_KEY'] || options[:keys]

  if ssh_config_file
    from_config_file = Net::SSH::Config.for(host,files=[ssh_config_file])
    options.merge!(from_config_file)
  end

  set :host,        options[:host_name] || host
  set :ssh_options, options

  # Disable become
  # set :become, false


  # Set environment variables
  # set :env, :LANG => 'C', :LC_MESSAGES => 'C'

  # Set PATH
  # set :path, '/sbin:/usr/local/sbin:$PATH'
when 'winrm'
#
# OS type: Windows
#
  set :backend, :winrm
  set :os, :family => 'windows'

  user = ENV['TARGET_USER']
  port = ENV['TARGET_PORT']
  pass = ENV['TARGET_PASSWORD']

  if user.nil?
    begin
      require 'highline/import'
    rescue LoadError
      fail "highline is not available. Try installing it."
    end
    user = ask("\nEnter #{host}'s login user: ") { |q| q.echo = true }
  end
  if pass.nil?
    begin
      require 'highline/import'
    rescue LoadError
      fail "highline is not available. Try installing it."
    end
    pass = ask("\nEnter #{user}@#{host}'s login password: ") { |q| q.echo = false }
  end

  endpoint = "http://#{host}:#{port}/wsman"

  winrm = ::WinRM::WinRMWebService.new(endpoint, :ssl, :user => user, :pass => pass, :basic_auth_only => true)
  winrm.set_timeout 300 # 5 minutes max timeout for any operation
  Specinfra.configuration.winrm = winrm

when 'local'
#
# local connection
#
    set :backend, :exec
end

TARGET_HOST_VARS_PATHが設定されていたら、Ansibleから読み込んだ変数を収めるディクショナリを更新するようにしただけ。

まだホスト名を設定するroleしか検証していないので、いろいろなタスク・複数のホストでうまく行くか検証を重ねてゆく。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です