ほわいとぼーど

ぷろぐらまのメモ帳

ServerspecのRakefileをカスタマイズしてみた話

いじってたの6月なので既にだいぶ忘れているのですが、
やりたかったこととしては、
①attributes.yml方式で、fqdnではなくIPベースで通信先を指定したい。
②複数host実行でもパスワード入力1回にしたい。
③指定したhostに対して実行できるようにする。

これらを実現するためにRuby初心者ながら1日くらいかけてRakefileをカスタマイズしてみました。
以下、それぞれ説明します。


①attributes.yml方式で、fqdnではなくIPベースで通信先を指定したい。

Serverspecのデフォルトは対象host毎にディレクトリを作って実行する方式ですが、
自分の場合はAdvancedTips(http://serverspec.org/advanced_tips.html)の中で紹介されている
roleをattributes.ymlで指定する方式で使用しています。
サイトの例の場合、各hostのfqdn(host.domain)からrakeタスクのsymbolを生成しているのですが、
symbolには"."(ドット)が使えないようなので、ドットで分割した1番目の要素をrakeタスク名としています。
これはfqdnの場合はだいたいうまくいきますが、
自分の場合ローカルではIPアドレスベースで通信しており、
IPアドレスの1番目を使うとほとんどのhostでrakeタスク名が重複してしまいます。
そこでattributes.ymlのkeyにあたる部分にはhostの略称を書くようにして、
それをそのままrakeタスク名に使うようにしました。
また、実際に通信するIPアドレスはhostnameというattributeで別に定義します。


②複数ノード実行でもパスワード入力1回にしたい。

Serverspecにはsudo時のパスワードを入力する方法が用意されているのですが、
SUDO_PASSWORD=xxxxxxxx は画面に平文で書くことになるのでなんとなく却下、
ASK_SUDO_PASSWORD=1 だとsudoが行われる毎にパスワード聞かれてしまいます。
(sudoer指定しろって話かもしれませんが)
こういうのは最初に一回入力を求められて後はその値を使って欲しい。
ということでLDAP=1を指定したら入力1回だけさせて後は聞かないようにしました。
名前のとおりLDAPを利用してる環境で使うことを想定していて、
複数環境(LDAP利用するhostとLDAP利用しないhost)混ぜて使うことは想定していません。
attributeでフラグ付ければ個別操作できるとは思いましたが、
用途的に混ぜて使うことは無いだろうなと。


③指定したノードに対して実行できるようにする。

attribute.ymlに10種類書いたからといって毎回全部に実行するとは限らない。
例えばAdvancedTipsのをそのまま利用した場合、
rake serverspec:hostname
みたいにすれば1個1個指定することは出来ます。
ただ、今回の場合②と組み合わせる必要があったのと当然複数指定もしたかったので、
rake spec HOST=host1,host2
みたいに指定できるようにしました。


②と③に関してはrakeタスクの仕組みがよくわからず苦労しました。
そんなこんなでRuby素人がカスタマイズしたRakefileは以下。


Rakefile

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

attributes = YAML.load_file('spec/attributes.yml')

desc "Run serverspec"
task :spec => 'serverspec:prepare'

namespace :serverspec do
  attributes.keys.each do |key|
    desc "Run serverspec to #{key}"
    RSpec::Core::RakeTask.new(key.split('.')[0].to_sym) do |t|
      ENV['TARGET_HOST'] = key
      t.pattern = 'spec/{' + attributes[key][:roles].join(',') + '}/*_spec.rb'
    end
  end
  
  desc "Run serverspec to all hosts"
  task :all => attributes.keys.map {|key| 'serverspec:' + key.split('.')[0] }
  
  desc "Run bofore task for serverspec"
  task :prepare do
    if ENV['LDAP']
      require 'highline/import'
      ENV['LDAP_PASSWORD'] = ask("Enter ldap password: ") { |q| q.echo = false }
      ENV['ASK_LDAP'] = '1'
    end
    
    if ENV['HOST']
      ENV['HOST'].split(",").map {|host| Rake::Task['serverspec:' + host.split('.')[0]].invoke }
    else
      Rake::Task['serverspec:all'].invoke
    end
  end
end

spec_helper.rb

require 'serverspec'
require 'pathname'
require 'net/ssh'
require 'yaml'

include Serverspec::Helper::Ssh
include Serverspec::Helper::DetectOS
include Serverspec::Helper::Attributes

attributes = YAML.load_file('spec/attributes.yml')

RSpec.configure do |c|
  c.color_enabled = true
  c.path = "/sbin:/usr/sbin"
  key    = ENV['TARGET_HOST']
  
  if ENV['ASK_SUDO_PASSWORD'] then
    require 'highline/import'
    c.sudo_password = ask("Enter sudo password: ") { |q| q.echo = false }
  elsif ENV['ASK_LDAP'] then
    c.sudo_password = ENV['LDAP_PASSWORD']
  else
    c.sudo_password = ENV['SUDO_PASSWORD']
  end
  
  c.host  = attributes[key][:hostname]
  attr_set attributes[key]
  options = Net::SSH::Config.for(c.host)
  user    = options[:user] || Etc.getlogin
  if ENV['ASK_LDAP']
    options[:password] = ENV['LDAP_PASSWORD']
  end
  c.ssh   = Net::SSH.start(c.host, user, options)
  c.os    = backend.check_os
end

attributes.yml

host1:
  :roles:
    - web
    - db
  :hostname: 192.168.100.10
host2:
  :roles:
    - web
  :hostname: 192.168.100.20


Serverspecの知見は既に一杯あるし、自分以外使ってない素人仕事なのでどうかと思いましたが、
これも何かの勉強になった足跡として残しておきます。
自分なりにrakeタスクを理解できた瞬間があったつもりなんだけど、
間違ってたら恥ずかしいので詳しくは書かないでおきます。
変更点を色表示できたりしたら良かったかなとおもいつつ終了。