ServerspecでWinRMクライアントをwinrs.exeとPsExec.exeに置き換えてみた

# はじめに
serverspecのwinrmクライアントとして「winrs.exe」を試してみました。その結果処理時間がかなり短縮されることがわかりました。
また、Sysinternalsに含まれる「PsExec.exe」も試してみましたが、こちらは実行するコマンドの文字列数に制限があるようでServerspecでは利用できないことがわかりました。

今回は「winrs.exe」の置き換えについて記載します。

# バージョン

記事作成で用いた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」で保存しています。

# WinRMクライアントの作成

winrmバックエンドからwinrs.exeを実行するためのクラスを作成します。
※Open3.capture3を使うと固まってしまうのでsystemuを使いました。

```ruby:C：\serverspec\sample\lib\CustomWinRs.rb
require 'rubygems'
require 'open3'
require 'systemu'
require 'kconv'

class CustomWinRs

  @my_name = 'CustomWinRs'

  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
    if @run_mode == "ps1"
      exec_cmd = "powershell -NoProfile -encodedCommand #{command}"
    else
      command  = "#{command}".gsub("\\\"","\"").gsub("\"","\\\"")
      exec_cmd = "powershell -NoProfile -command \"#{command}\""
    end
    exec_cmd     = "#{@winrm_client} -r:#{@host_name} -u:#{@user_name} -p:#{@user_pass} #{exec_cmd}"
    # systemuの不具合回避
    case "#{exec_cmd}".length
      when 21,282,3866 then
        exec_cmd = "#{@winrm_client} -remote:#{@host_name} -u:#{@user_name} -p:#{@user_pass} #{exec_cmd}"
    end
    begin
      # stdout, stderr, status  = Open3.capture3(exec_cmd)
      status, stdout, stderr = systemu "#{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
```

spec_helper.rbでWinRMクライアントを変更します。

```ruby:C：\serverspec\sample\spec\spec_helper.rb
require 'serverspec'
require 'yaml'
WINRM_CLIENT_GEM_WINRM = 1
WINRM_CLIENT_PS1_WINRM = 2
WINRM_CLIENT_CSP_WINRM = 3
WINRM_CLIENT_EXE_WINRS = 4
WINRM_CLIENT_EXE_PSEXE = 5
winrm_client = WINRM_CLIENT_EXE_WINRS
params = {}
params['hostname'] = ENV['TARGET_HOST']
params['run_env'] = "development"
puts ""
puts "##################################################"
puts "HOST: #{params['hostname']} : #{params['run_env']} : winrm_client = #{winrm_client}"
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
case winrm_client
  when WINRM_CLIENT_GEM_WINRM
    require 'winrm'
    endpoint = "http://#{ENV['TARGET_HOST']}:5985/wsman"
    winrm = ::WinRM::WinRMWebService.new(endpoint, :ssl, :user => "#{properties[:winrm_user]}", :pass => "#{properties[:winrm_pass]}", :basic_auth_only => true)
    winrm.set_timeout 300
  when WINRM_CLIENT_PS1_WINRM
    require 'CustomWinRm'
    winrm   = CustomWinRm.new({"user" => "#{properties[:winrm_user]}", "pass" => "#{properties[:winrm_pass]}", "host" => "#{ENV['TARGET_HOST']}", "winrm" => ".\\lib\\WinRm.ps1"})
  when WINRM_CLIENT_CSP_WINRM
    require 'CustomWinRm'
    winrm  = CustomWinRm.new({"user" => "#{properties[:winrm_user]}", "pass" => "#{properties[:winrm_pass]}", "host" => "#{ENV['TARGET_HOST']}", "winrm" => ".\\lib\\rmtcmd.exe"})
  when WINRM_CLIENT_EXE_WINRS
    require 'CustomWinRs'
    winrm   = CustomWinRs.new({"user" => "#{properties[:winrm_user]}", "pass" => "#{properties[:winrm_pass]}", "host" => "#{ENV['TARGET_HOST']}", "winrm" => "C:\\Windows\\system32\\winrs.exe"})
    Specinfra.configuration.os_encoding = "Windows-31J"
    Specinfra.configuration.file_encoding = "Shift-JIS"
    Specinfra.configuration.script_encoding = "Windows-31J"
  when WINRM_CLIENT_EXE_PSEXE
    require 'CustomPsExec'
    winrm   = CustomPsExec.new({"user" => "#{properties[:winrm_user]}", "pass" => "#{properties[:winrm_pass]}", "host" => "#{ENV['TARGET_HOST']}", "winrm" => "C:\\Tool\\SysinternalsSuite\\PsExec.exe"})
end
Specinfra.configuration.winrm = winrm
```

# SpecInfraの修正

以前書いた記事[Serverspecで使うWinRMクライアントをInvoke-commandに置き換えてみた](http://qiita.com/h-ymmr/items/627aa60b57171f67d295)で修正した「lib\specinfra\backend\winrm.rb」以外に以下のファイルを修正します。

```diff:lib\specinfra\command\windows\base\file.rb
46c46,51
<       %Q!Get-Content("#{file}") | Out-String!
---
>       # MODIFIED
>       if Specinfra.configuration.file_encoding.nil?
>         %Q!Get-Content("#{file}") | Out-String!
>       else
>         %Q![Io.File]::ReadAllText("#{file}", [System.Text.Encoding]::GetEncoding("#{Specinfra.configuration.file_encod
ing}"))!
>       end
91c96,101
<         exec %Q!(Get-Content("#{file}") | Out-String) -match '#{convert_regexp(pattern)}'!
---
>         # MODIFIED
>         if Specinfra.configuration.file_encoding.nil?
>           exec %Q!(Get-Content("#{file}") | Out-String) -match '#{convert_regexp(pattern)}'!
>         else
>           exec %Q![Io.File]::ReadAllText("#{file}", [System.Text.Encoding]::GetEncoding("#{Specinfra.configuration.fil
e_encoding}")) -match "#{convert_regexp(pattern)}"!
>         end
100c110,115
<         exec %Q!(CropText -text (Get-Content("#{file}") | Out-String) -fromPattern '#{convert_regexp(from)}' -toPatter
n '#{convert_regexp(to)}') -match '#{pattern}'!
---
>         # MODIFIED
>         if Specinfra.configuration.file_encoding.nil?
>           exec %Q!(CropText -text (Get-Content("#{file}") | Out-String) -fromPattern '#{convert_regexp(from)}' -toPatt
ern '#{convert_regexp(to)}') -match '#{pattern}'!
>         else
>           exec %Q!(CropText -text ([Io.File]::ReadAllText("#{file}"), [System.Text.Encoding]::GetEncoding("#{Specinfra
.configuration.file_encoding}")) -fromPattern "#{convert_regexp(from)}" -toPattern "#{convert_regexp(to)}") -match "#{pa
ttern}"!
>         end
```

```diff:lib\specinfra\backend\powershell\script_helper.rb
40c40,44
<             script_text = script_text.encode('ASCII-8BIT')
---
>             if Specinfra.configuration.script_encoding.nil?
>               script_text = script_text.encode('ASCII-8BIT')
>             else
>               script_text = script.strip.encode('UTF-16LE', Specinfra.configuration.script_encoding)
>             end
```

# Serverspecの修正

Serverspec側も少しだけ修正を加えていきます。

```diff:lib\serverspec.rb
61a62,80
>     # ADD
>     # rspec-core-3.4.1/lib/rspec/core/formatters/exception_presenter.rb:77:in `join': incompatible character encodings: Windows-31J and UTF-8 (Encoding::CompatibilityError)
>     def fully_formatted(failure_number, colorizer=::RSpec::Core::Formatters::ConsoleCodes)
>       lines = fully_formatted_lines(failure_number, colorizer)
>       # lines.join("\n") << "\n"
>       tmp = ""
>       lines.each {|line| tmp << "#{line}\n".encode('UTF-8')}
>       tmp
>     end
>   end
> end
>
> # ADD
> # rspec-core-3.4.1/lib/rspec/core/source.rb:23:in `split': invalid byte sequence in Windows-31J (ArgumentError)
> if defined?(RSpec::Core::Source)
>   class RSpec::Core::Source
>     def lines
>       @lines ||= source.scrub('?').split("\n")
>     end
```

```diff:lib\serverspec\type\command.rb
0a1
> require "kconv"     # ADD
4c5,8
<       command_result.stdout
---
>       # MODIFIED
>       # command_result.stdout
>       # Kconv.toutf8( "#{command_result.stdout}" ).encode("Windows-31J")
>       return_result(command_result.stdout)
8c12,15
<       command_result.stderr
---
>       # MODIFIED
>       # command_result.stderr
>       # Kconv.toutf8( "#{command_result.stderr}" ).encode("Windows-31J")
>       return_result(command_result.stderr)
18a26,35
>
>     # MODIFIED
>     def return_result(result)
>       if Specinfra.configuration.os_encoding.nil?
>         result
>       else
>         Kconv.toutf8( "#{result}" ).encode(Specinfra.configuration.os_encoding)
>       end
>     end
>
```

```diff:lib\serverspec\type\file.rb
1a2
> require "kconv"     # ADD
113c114,119
<       @content
---
>       # MODIFIED
>       if Specinfra.configuration.os_encoding.nil?
>         @content
>       else
>         Kconv.toutf8("#{@content}").encode(Specinfra.configuration.os_encoding)
>       end
```

# テストの実行

それでは実行してみます。

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

##################################################
HOST: REMOTEHOST : development : winrm_client = 4
##################################################

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 6.5 seconds (files took 1.29 seconds to load)
6 examples, 0 failures

PS C:\serverspec\sample>
```

かなり処理時間が短縮されました。