diff --git a/beaker.gemspec b/beaker.gemspec index ec78d688c2..8873ea725e 100644 --- a/beaker.gemspec +++ b/beaker.gemspec @@ -38,6 +38,8 @@ Gem::Specification.new do |s| s.add_runtime_dependency 'net-scp', '~> 1.2' s.add_runtime_dependency 'inifile', '~> 2.0' s.add_runtime_dependency 'rsync', '~> 1.0.9' + s.add_runtime_dependency 'winrm', '~> 1.3.0' + s.add_runtime_dependency 'winrm-fs', '~> 0.1.0' # Optional provisioner specific support s.add_runtime_dependency 'rbvmomi', '~> 1.8' diff --git a/lib/beaker/host.rb b/lib/beaker/host.rb index 9ff8ab948a..59ec39b51c 100644 --- a/lib/beaker/host.rb +++ b/lib/beaker/host.rb @@ -3,7 +3,7 @@ require 'benchmark' require 'rsync' -[ 'command', 'ssh_connection' ].each do |lib| +[ 'command', 'ssh_connection', 'winrm_connection' ].each do |lib| require "beaker/#{lib}" end @@ -358,9 +358,14 @@ def delete_env_var key, val end def connection - @connection ||= SshConnection.connect( reachable_name, + if self[:platform] =~ /windows-/ and self[:communicator] == 'winrm' + @connection ||= WinrmConnection.connect( reachable_name, + self['winrm'], { :logger => @logger } ) + else + @connection ||= SshConnection.connect( reachable_name, self['user'], self['ssh'], { :logger => @logger } ) + end end def close diff --git a/lib/beaker/options/presets.rb b/lib/beaker/options/presets.rb index a389de652c..4f5d35ea44 100644 --- a/lib/beaker/options/presets.rb +++ b/lib/beaker/options/presets.rb @@ -211,6 +211,11 @@ def presets :forward_agent => true, :keys => ["#{ENV['HOME']}/.ssh/id_rsa"], :user_known_hosts_file => "#{ENV['HOME']}/.ssh/known_hosts", + }, + :winrm => { + :auth => 'kerberos', + :timeout => 300, + :port => 5985 } }) end diff --git a/lib/beaker/winrm_connection.rb b/lib/beaker/winrm_connection.rb new file mode 100644 index 0000000000..3e37cde6a3 --- /dev/null +++ b/lib/beaker/winrm_connection.rb @@ -0,0 +1,184 @@ +require 'socket' +require 'timeout' +require 'winrm' +require 'winrm-fs' + +module Beaker + class WinrmConnection + + attr_accessor :logger + + RETRYABLE_EXCEPTIONS = [ + SocketError, + Timeout::Error, + Errno::ETIMEDOUT, + Errno::EHOSTDOWN, + Errno::EHOSTUNREACH, + Errno::ECONNREFUSED, + Errno::ECONNRESET, + Errno::ENETUNREACH, + IOError, + ] + + def initialize hostname, winrm_opts = {}, options ={} + @hostname = hostname + @winrm_opts = winrm_opts + @logger = options[:logger] + @options = options + end + + def self.connect hostname, winrm_opts = {}, options ={} + connection = new hostname, winrm_opts, options + connection.connect + connection + end + + # connect to the host + def connect + try = 1 + last_wait = 0 + wait = 1 + @winrm ||= begin + @logger.debug "Attempting winrm connection to #{@hostname}, opts: #{@winrm_opts}" + endpoint = "http://#{@hostname}:5985/wsman" + auth = @winrm_opts['auth'] + + case auth + when 'kerberos' + krb5_realm = @winrm_opts['realm'] + WinRM::WinRMWebService.new(endpoint, :kerberos, :realm => krb5_realm) + when 'plaintext' + user = @winrm_opts['user'] + pass = @winrm_opts['pass'] + WinRM::WinRMWebService.new(endpoint, :plaintext, :user => user, :pass => pass, :basic_auth_only => true) + when 'ssl' + user = @winrm_opts['user'] + pass = @winrm_opts['pass'] + ca_trust_path = @winrm_opts['ca_trust_path'] + WinRM::WinRMWebService.new(endpoint, :ssl, :user => user, :pass => pass, :ca_trust_path => ca_trust_path, :basic_auth_only => true) + else + @logger.error "Invalid authentication method #{auth}, supported: kerberos, plaintext, ssl." + raise + end + rescue *RETRYABLE_EXCEPTIONS => e + if try <= 11 + @logger.warn "Try #{try} -- Host #{@hostname} unreachable: #{e.class.name} - #{e.message}" + @logger.warn "Trying again in #{wait} seconds" + sleep wait + (last_wait, wait) = wait, last_wait + wait + try += 1 + retry + else + @logger.error "Failed to connect to #{@hostname}" + raise + end + end + end + + # closes this winrmConnection + def close + begin + if @winrm and @sid + @winrm.close_shell(@sid) + else + @logger.warn("winrm.close: connection is already closed, no action needed") + end + rescue *RETRYABLE_EXCEPTIONS => e + @logger.warn "Attemped winrm.close, (caught #{e.class.name} - #{e.message})." + rescue => e + @logger.warn "winrm.close threw unexpected Error: #{e.class.name} - #{e.message}. Shutting down, and re-raising error below" + raise e + ensure + @winrm = nil + @logger.warn("winrm connection to #{@hostname} has been terminated") + end + end + + def execute command, options= {}, stdout + try = 1 + wait = 1 + last_wait = 0 + output = WinRM::Output.new + result = Result.new(@hostname, command) + + begin + # ensure that we have a current connection object + connect + @sid = @winrm.open_shell + if @sid + @logger.info "Open a shell on #{@hostname} for #{command.inspect}" + cmd_id = @winrm.run_command(@sid, command) + output = @winrm.get_command_output(@sid, cmd_id) + result.stdout = output.stdout + result.stderr = output.stderr + result.exit_code = output[:exitcode] + + @logger.debug "STDOUT: #{result.stdout}" unless result.stdout.nil? || result.stdout == '' + @logger.debug "STDERR: #{result.stderr}" unless result.stderr.nil? || result.stderr == '' + @logger.debug "Exit Code: #{result.exit_code}" unless result.exit_code.nil? || result.exit_code == '' + + else + abort "FAILED: could not open a shell on #{@hostname} for #{command.inspect}" + end + rescue *RETRYABLE_EXCEPTIONS => e + if try < 11 + sleep wait + (last_wait, wait) = wait, last_wait + wait + try += 1 + @logger.error "Command execution '#{@hostname}$ #{command}' failed (#{e.class.name} - #{e.message})" + close + @logger.debug "Preparing to retry: closed winrm object" + retry + else + raise + end + end + result.finalize! + @logger.last_result = result + result + # Close the shell to avoid the quota of 5 concurrent shells for a user by default. + close + @sid = nil + end + + def scp_to source, target, options = {}, dry_run = false + return if dry_run + + result = Result.new(@hostname, [source, target]) + result.stdout = "\n" + file_manager = WinRM::FS::FileManager.new(@winrm) + file_manager.upload(source, target) do |bytes_copied, total_bytes, local_path, remote_path| + result.stdout << "\t#{bytes_copied}bytes of #{total_bytes}bytes copied\n" + end + + # Setting these values allows reporting via result.log(test_name) + result.stdout << " Copied file #{source} to #{@hostname}:#{target}" + + # Net::Scp always returns 0, so just set the return code to 0. + result.exit_code = 0 + + result.finalize! + return result + end + + def scp_from source, target, options = {}, dry_run = false + return if dry_run + + result = Result.new(@hostname, [source, target]) + result.stdout = "\n" + file_manager = WinRM::FS::FileManager.new(@winrm) + file_manager.download(source, target) do |bytes_copied, total_bytes, local_path, remote_path| + result.stdout << "\t#{bytes_copied}bytes of #{total_bytes}bytes copied\n" + end + + # Setting these values allows reporting via result.log(test_name) + result.stdout << " Copied file #{@hostname}:#{source} to #{target}" + + # Net::Scp always returns 0, so just set the return code to 0. + result.exit_code = 0 + + result.finalize! + result + end + end +end \ No newline at end of file