はじめに
WindowsでServerspecを使ってみました。
バージョン
記事作成で用いたOS・モジュールのバージョンはこちらです。
- Windows 7 Pro SP1 32bit 日本語版
- ruby 2.2.3p173 (2015-08-18 revision 51636) [i386-mingw32]
- rspec (3.4.0)
- rspec-core (3.4.1)
- rspec-its (1.2.0)
- serverspec (2.26.0)
- specinfra (2.47.0)
※rubyスクリプトは全て文字コードを「UTF-8」で保存しています。
※バッチファイルは「Shift-JIS」で保存しています。
まずはirbからWinRMを使ってみる
ドメインアカウントは接続エラーになるので、ローカルアカウントで接続してみます。
PS C:\serverspec\sample> irb
irb(main):001:0> require 'winrm'
=> true
irb(main):002:0> require "kconv"
=> true
irb(main):003:0> endpoint = "http://remotehost:5985/wsman"
=> "http://remotehost:5985/wsman"
irb(main):004:0> objWinRm = WinRM::WinRMWebService.new(endpoint, :plaintext, :user=>"localuser1", :pass=>"password", :basic_auth_only=>true, :disable_sspi=>true)
=> #<WinRM::WinRMWebService:0x233d780 (省略)>
irb(main):005:0> objWinRm.locale("ja-JP")
=> "ja-JP"
irb(main):006:0>
日本語を含むファイルを表示してみます。
irb(main):007:0* status = objWinRm.powershell("type C:\\serverspec\\test.cmd") do |stdout, stderr|
irb(main):008:1* print Kconv.tosjis( stdout ) if stdout
irb(main):009:1> print Kconv.tosjis( stderr ) if stderr
irb(main):010:1> end
echo あいうえお
exit 0
=> {:data=>[{:stdout=>"echo \u3042\u3044\u3046\u3048\u304A"}, {:stdout=>"\r\n"}, {:stdout=>"exit 0\r\n"}], :exitcode=>0}
問題なく日本語が表示されました。
次にバッチファイルを実行してみます。
irb(main):011:0> status = objWinRm.powershell("C:\\serverspec\\test.cmd") do |stdout, stderr|
irb(main):012:1* print Kconv.tosjis( stdout ) if stdout
irb(main):013:1> print Kconv.tosjis( stderr ) if stderr
irb(main):014:1> end
C:\Users\localuser1>echo 鐃緒申鐃緒申鐃緒申鐃緒申鐃緒申鐃緒申鐃緒申鐃緒申鐃緒申鐃緒申
C:\Users\localuser1>exit 0
=> {:data=>[{:stdout=>"\r\n"}, {:stdout=>"C:\\Users\\localuser1>echo \uFFFD\uFFFD\uFFFD\uFFFD\uFFFD\uFFFD\uFFFD\uFFFD\uFFFD\uFFFD\r\n\uFFFD\uFFFD\uFFFD\uFFFD\uFFFD\uFFFD\uFFFD\uFFFD\uFFFD\uFFFD\r\n"}, {:stdout=>"\r\nC:\\Users\\localuser1>exit 0 \r\n"}], :exitcode=>0}
irb(main):015:0>
英語ロケールで実行されてしまうのか国際化に対応していないコマンドを実行すると文字化けが起こるようです。
Invoke-CommandでWinRMクライアントを作る
winrmバックエンドからPowershellを実行するためクラスを作成します。
require 'rubygems'
require 'open3'
require 'kconv'
class CustomWinRm
@my_name = 'CustomWinRm'
def initialize(args = {})
@user_name = args['user'] if args.key?('user')
@user_pass = args['pass'] if args.key?('pass')
@host_name = args['host'] if args.key?('host')
@winrm_client = args['winrm'] if args.key?('winrm')
@run_mode = args['mode'] if args.key?('mode')
end
def run_command(command)
retcode = -1
exec_cmd = "#{@winrm_client} -h #{@host_name} -u #{@user_name} -p #{@user_pass}"
if @run_mode == "ps1"
exec_cmd << " -es \"#{command}\""
else
command = "#{command}".gsub("\\\"","\"").gsub("\"","\\\"")
exec_cmd << " -cmd \"#{command}\""
end
exec_cmd = "powershell -NoProfile -command \"#{exec_cmd}\"; exit $LASTEXITCODE" if ".ps1" == File.extname("#{@winrm_client}")
begin
stdout, stderr, status = Open3.capture3(exec_cmd)
retcode = status.exitstatus if ! status.nil?
retcode = -1 if retcode.nil? || "#{retcode}" == ''
rescue Exception => excptn
puts Kconv.tosjis( "EXCEPTION : #{excptn.message}" )
retcode = -1
stdout = ""
stderr = excptn.message
end
return {:exitcode => retcode, :stdout => stdout, :stderr => stderr}
end
def run_cmd(command)
@run_mode = "cmd"
return run_command(command)
end
alias :cmd :run_cmd
def run_powershell_script(command)
@run_mode = "ps1"
return run_command(Base64.strict_encode64("#{command}".encode('UTF-16LE')))
end
alias :powershell :run_powershell_script
end
WinRMクライアント本体となるPowershellスクリプトを作成します。
param (
[string]$h = "${ENV:COMPUTERNAME}",
[string]$u = "${ENV:COMPUTERNAME}\Administrator",
[string]$p = "ここにパスワード",
[string]$c = "hostname",
[string]$cmd,
[string]$es
)
set-variable -name RET_SUCCESS -value 0 -option constant
set-variable -name RET_EXCPTN -value -1 -option constant
$ErrorActionPreference = "stop"
$Error.Clear()
$retcd = $RET_SUCCESS
$cmdstatus = ""
if (![String]::IsNullOrEmpty($cmd)) {$c = "powershell -NoProfile -command `"${cmd}; exit $LASTEXITCODE`""}
if (![String]::IsNullOrEmpty($es)) {$c = "powershell -NoProfile -encodedCommand ${es}"}
try {
$password = ConvertTo-SecureString $p -asplaintext -force
$cred = New-Object System.Management.Automation.PsCredential $u, $password
$session = New-PSSession -ComputerName $h -credential $cred
Invoke-Command -Session $session -ScriptBlock {cmd /c $args[0]} -args $c
$cmdstatus = Invoke-Command -Session $session -ScriptBlock {$LASTEXITCODE}
$retcd = [int]$cmdstatus
} catch [Exception] {
$retcd = $RET_EXCPTN
Write-Output $_.Exception.Message
} finally {
if ( $NULL -ne $session ) {Remove-PSSession -Session $session}
}
Write-Output ""
exit $retcd
irbから実行してみます。
PS C:\serverspec\sample> irb
irb(main):001:0> $LOAD_PATH << "C:\\serverspec\\sample\\lib"
=> [(省略), "C:\\serverspec\\sample\\lib"]
irb(main):002:0> require 'CustomWinRm'
=> true
irb(main):003:0> winrm = CustomWinRm.new({"user" => "mydomain\\domainuser1", "pass" => "password", "host" => "remotehost", "winrm" => ".\\lib\\WinRm.ps1"})
=> #<CustomWinRm:0x25b5a30 @user_name="mydomain\\domainuser1", @user_pass="パスワード", @host_name="remotehost", @winrm_client=".\\lib\\WinRm.ps1">
irb(main):004:0> winrm.run_command("C:\\serverspec\\test.cmd")
=> {:exitcode=>0, :stdout=>"\nC:\\Users\\domainuser1\\Documents>echo あいうえお \nあいうえお\n\nC:\\Users\\domainuser1\\Documents>exit 0 \n\n", :stderr=>""}
irb(main):005:0>
今度は文字化けせずにすみました。またドメインアカウントで接続することができました。
Serverspecで実行してみる
SpecInfraをちょっとだけ修正します。
15,16c15,21
< stdout, stderr = [:stdout, :stderr].map do |s|
< result[:data].select {|item| item.key? s}.map {|item| item[s]}.join
---
> if Specinfra.configuration.customwinrm.nil? || ! Specinfra.configuration.customwinrm
> stdout, stderr = [:stdout, :stderr].map do |s|
> result[:data].select {|item| item.key? s}.map {|item| item[s]}.join
> end
> else
> stdout = result[:stdout]
> stderr = result[:stderr]
spec_helper.rbでデフォルトのWinRMを今回作成したものへ置き換えます。
require 'serverspec'
require 'yaml'
require 'CustomWinRm'
params = {}
params['hostname'] = ENV['TARGET_HOST']
params['run_env'] = "development"
puts ""
puts "##################################################"
puts "HOST: #{params['hostname']} : #{params['run_env']}"
puts "##################################################"
properties = YAML.load_file("env/properties.#{params['run_env']}.yml")
set_property = properties
set :backend, :winrm
set :os, :family => 'windows'
Specinfra.configuration.customwinrm = true
winrm = CustomWinRm.new({"user" => "#{properties[:winrm_user]}", "pass" => "#{properties[:winrm_pass]}", "host" => "#{ENV['TARGET_HOST']}", "winrm" => ".\\lib\\WinRm.ps1"})
Specinfra.configuration.winrm = winrm
テストケースでは「標準出力内容の確認」と「ファイル内容の確認」を試してみます。
require 'spec_helper'
require 'CustomHelper'
describe command ("ipconfig /all | Select-String #{'ノード'.encode('Windows-31J')}") do
its(:stdout) { should match /#{"ハイブリッド".encode("Windows-31J")}/}
end
describe file("C:\\serverspec\\test.cmd") do
it { should be_file }
it { should contain "#{'あいうえお'.encode('Windows-31J')}" }
it { should_not contain "#{'かきくけこ'.encode('Windows-31J')}" }
its(:content) { should match /#{"あいうえお".encode("Windows-31J")}/ }
its(:content) { should_not match /#{"かきくけこ".encode("Windows-31J")}/ }
end
その他、serverspecの実行に必要なファイルを用意します。
require 'rake'
require 'rspec/core/rake_task'
require 'yaml'
servers = YAML.load_file("env/servers.yml")
task :spec => 'spec:all'
task :default => :spec
namespace :spec do
task :all => servers.keys.map {|key| 'spec:' + key }
servers.keys.each do |key|
desc "Run serverspec to #{key}"
RSpec::Core::RakeTask.new(key.to_sym) do |t|
ENV['TARGET_HOST'] = key
role_pattern = servers[key][:roles].join(',')
t.pattern = "spec/{" + role_pattern + "}/*_spec.rb"
t.fail_on_error = false
end
end
end
REMOTEHOST:
:roles:
- sample
:winrm_user: localuser1
:winrm_pass: password
実行してみます。
PS C:\serverspec\sample> rake
##################################################
HOST: REMOTEHOST : development
##################################################
Command "ipconfig /all | Select-String ノード"
stdout
should match /ハイブリッド/
File "C:\serverspec\test.cmd"
should be file
should contain "あいうえお"
should not contain "かきくけこ"
content
should match /あいうえお/
content
should not match /かきくけこ/
Finished in 16.65 seconds (files took 1.3 seconds to load)
6 examples, 0 failures
PS C:\serverspec\sample>
うまくいきました。
但し、デフォルトのWinRMを使うよりもかなり遅いです。
672ケース(168ケースx4サーバー)のテスト所要時間は次の通りです。
- デフォルトのWinRM:427秒
- Invoke-Command:541秒
さらに便利に使うためには
以下の記事に処理速度向上についてまとめましたので、ぜひ参照下さい。