communicators/winrm: initial import from vagrant-windows

This commit is contained in:
Mitchell Hashimoto 2014-03-10 22:38:31 -07:00
parent f6a5e20688
commit 2a8a04ebb9
7 changed files with 431 additions and 0 deletions

View File

@ -0,0 +1,131 @@
require "timeout"
require "log4r"
require_relative "shell"
module VagrantPlugins
module CommunicatorWinRM
# Provides communication channel for Vagrant commands via WinRM.
class Communicator < Vagrant.plugin("2", :communicator)
def self.match?(machine)
# This is useless, and will likely be removed in the future (this
# whole method).
true
end
def initialize(machine)
@machine = machine
@logger = Log4r::Logger.new("vagrant::communication::winrm")
@shell = nil
@logger.debug("Initializing WinRMCommunicator")
end
def ready?
@logger.debug("Checking whether WinRM is ready...")
Timeout.timeout(@machine.config.winrm.timeout) do
shell.powershell("hostname")
end
@logger.info("WinRM is ready!")
return true
rescue Vagrant::Errors::VagrantError => e
# We catch a `VagrantError` which would signal that something went
# wrong expectedly in the `connect`, which means we didn't connect.
@logger.info("WinRM not up: #{e.inspect}")
# We reset the shell to trigger calling of winrm_finder again.
# This resolves a problem when using vSphere where the ssh_info was not refreshing
# thus never getting the correct hostname.
@shell = nil
return false
end
def shell
@shell ||= create_shell
end
def execute(command, opts={}, &block)
opts = {
:error_check => true,
:error_class => Errors::ExecutionError,
:error_key => :execution_error,
:command => command,
:shell => :powershell
}.merge(opts || {})
exit_status = do_execute(command, opts[:shell], &block)
if opts[:error_check] && exit_status != 0
raise_execution_error(opts, exit_status)
end
exit_status
end
alias_method :sudo, :execute
def test(command, opts=nil)
@logger.debug("Testing: #{command}")
# HACK: to speed up Vagrant 1.2 OS detection, skip checking for *nix OS
return false unless (command =~ /^uname|^cat \/etc|^cat \/proc|grep 'Fedora/).nil?
opts = { :error_check => false }.merge(opts || {})
execute(command, opts) == 0
end
def upload(from, to)
@logger.debug("Uploading: #{from} to #{to}")
shell.upload(from, to)
end
def download(from, to)
@logger.debug("Downloading: #{from} to #{to}")
shell.download(from, to)
end
protected
# This creates anew WinRMShell based on the information we know
# about this machine.
def create_shell
host_address = @machine.config.winrm.host
if !host_address
ssh_info = @machine.ssh_info
raise Errors::WinRMNotReady if !ssh_info
host_address = ssh_info[:host]
end
host_port = @machine.config.winrm.port
if @machine.config.winrm.guest_port
# TODO: search by guest port
end
WinRMShell.new(
host_address,
@machine.config.winrm.username,
@machine.config.winrm.password,
port: host_port,
timeout_in_seconds: @machine.config.winrm.timeout,
max_tries: @machine.config.winrm.max_tries,
)
end
def do_execute(command, shell, &block)
if shell.eql? :cmd
shell.cmd(command, &block)[:exitcode]
else
command = VagrantWindows.load_script("command_alias.ps1") << "\r\n" << command << "\r\nexit $LASTEXITCODE"
shell.powershell(command, &block)[:exitcode]
end
end
def raise_execution_error(opts, exit_code)
# The error classes expect the translation key to be _key, but that makes for an ugly
# configuration parameter, so we set it here from `error_key`
msg = "Command execution failed with an exit code of #{exit_code}"
error_opts = opts.merge(:_key => opts[:error_key], :message => msg)
raise opts[:error_class], error_opts
end
end #WinRM class
end
end

View File

@ -0,0 +1,46 @@
module VagrantPlugins
module CommunicatorWinRM
class Config < Vagrant.plugin("2", :config)
attr_accessor :username
attr_accessor :password
attr_accessor :host
attr_accessor :port
attr_accessor :guest_port
attr_accessor :max_tries
attr_accessor :timeout
def initialize
@username = UNSET_VALUE
@password = UNSET_VALUE
@host = UNSET_VALUE
@port = UNSET_VALUE
@guest_port = UNSET_VALUE
@max_tries = UNSET_VALUE
@timeout = UNSET_VALUE
end
def finalize!
@username = "vagrant" if @username == UNSET_VALUE
@password = "vagrant" if @password == UNSET_VALUE
@host = nil if @host == UNSET_VALUE
@port = 5985 if @port == UNSET_VALUE
@guest_port = 5985 if @guest_port == UNSET_VALUE
@max_tries = 20 if @max_tries == UNSET_VALUE
@timeout = 1800 if @timeout == UNSET_VALUE
end
def validate(machine)
errors = []
errors << "winrm.username cannot be nil." if machine.config.winrm.username.nil?
errors << "winrm.password cannot be nil." if machine.config.winrm.password.nil?
errors << "winrm.port cannot be nil." if machine.config.winrm.port.nil?
errors << "winrm.guest_port cannot be nil." if machine.config.winrm.guest_port.nil?
errors << "winrm.max_tries cannot be nil." if machine.config.winrm.max_tries.nil?
errors << "winrm.timeout cannot be nil." if machine.config.winrm.timeout.nil?
{ "WinRM" => errors }
end
end
end
end

View File

@ -0,0 +1,26 @@
module VagrantPlugins
module CommunicatorWinRM
module Errors
# A convenient superclass for all our errors.
class WinRMError < Vagrant::Errors::VagrantError
error_namespace("vagrant_winrm.errors")
end
class AuthError < WinRMError
error_key(:auth_error)
end
class ExecutionError < WinRMError
error_key(:execution_error)
end
class InvalidShell < WinRMError
error_key(:invalid_shell)
end
class WinRMNotReady < WinRMError
error_key(:winrm_not_ready)
end
end
end
end

View File

@ -0,0 +1,36 @@
require "vagrant"
module VagrantPlugins
module CommunicatorWinRM
autoload :Errors, File.expand_path("../errors", __FILE__)
class Plugin < Vagrant.plugin("2")
name "winrm communicator"
description <<-DESC
This plugin allows Vagrant to communicate with remote machines using
WinRM.
DESC
communicator("winrm") do
require File.expand_path("../communicator", __FILE__)
init!
Communicator
end
config("winrm") do
require_relative "config"
Config
end
protected
def self.init!
return if defined?(@_init)
I18n.load_path << File.expand_path(
"templates/locales/comm_winrm.yml", Vagrant.source_root)
I18n.reload!
@_init = true
end
end
end
end

View File

@ -0,0 +1,169 @@
require "timeout"
require "log4r"
require "winrm"
require "vagrant/util/retryable"
module VagrantPlugins
module CommunicatorWinRM
class WinRMShell
include Vagrant::Util::Retryable
# These are the exceptions that we retry because they represent
# errors that are generally fixed from a retry and don't
# necessarily represent immediate failure cases.
@@exceptions_to_retry_on = [
HTTPClient::KeepAliveDisconnected,
WinRM::WinRMHTTPTransportError,
Errno::EACCES,
Errno::EADDRINUSE,
Errno::ECONNREFUSED,
Errno::ECONNRESET,
Errno::ENETUNREACH,
Errno::EHOSTUNREACH,
Timeout::Error
]
attr_reader :logger
attr_reader :username
attr_reader :password
attr_reader :host
attr_reader :port
attr_reader :timeout_in_seconds
attr_reader :max_tries
def initialize(host, username, password, options = {})
@logger = Log4r::Logger.new("vagrant::communication::winrmshell")
@logger.debug("initializing WinRMShell")
@host = host
@port = options[:port] || 5985
@username = username
@password = password
@timeout_in_seconds = options[:timeout_in_seconds] || 60
@max_tries = options[:max_tries] || 20
end
def powershell(command, &block)
execute_shell(command, :powershell, &block)
end
def cmd(command, &block)
execute_shell(command, :cmd, &block)
end
def wql(query)
execute_wql(query)
end
def upload(from, to)
@logger.debug("Uploading: #{from} to #{to}")
file_name = (cmd("echo %TEMP%\\winrm-upload-#{rand()}"))[:data][0][:stdout].chomp
powershell <<-EOH
if(Test-Path #{to}) {
rm #{to}
}
EOH
Base64.encode64(IO.binread(from)).gsub("\n",'').chars.to_a.each_slice(8000-file_name.size) do |chunk|
out = cmd("echo #{chunk.join} >> \"#{file_name}\"")
end
powershell <<-EOH
mkdir $([System.IO.Path]::GetDirectoryName(\"#{to}\"))
$base64_string = Get-Content \"#{file_name}\"
$bytes = [System.Convert]::FromBase64String($base64_string)
$new_file = [System.IO.Path]::GetFullPath(\"#{to}\")
[System.IO.File]::WriteAllBytes($new_file,$bytes)
EOH
end
def download(from, to)
@logger.debug("Downloading: #{from} to #{to}")
output = powershell("[System.convert]::ToBase64String([System.IO.File]::ReadAllBytes(\"#{from}\"))")
contents = output[:data].map!{|line| line[:stdout]}.join.gsub("\\n\\r", '')
out = Base64.decode64(contents)
IO.binwrite(to, out)
end
protected
def execute_shell(command, shell=:powershell, &block)
raise Errors::InvalidShell, shell: shell unless shell == :cmd || shell == :powershell
begin
execute_shell_with_retry(command, shell, &block)
rescue => e
raise_winrm_exception(e, shell, command)
end
end
def execute_shell_with_retry(command, shell, &block)
retryable(:tries => @max_tries, :on => @@exceptions_to_retry_on, :sleep => 10) do
@logger.debug("#{shell} executing:\n#{command}")
output = session.send(shell, command) do |out, err|
block.call(:stdout, out) if block_given? && out
block.call(:stderr, err) if block_given? && err
end
@logger.debug("Exit status: #{output[:exitcode].inspect}")
return output
end
end
def execute_wql(query)
retryable(:tries => @max_tries, :on => @@exceptions_to_retry_on, :sleep => 10) do
@logger.debug("#executing wql: #{query}")
output = session.wql(query)
@logger.debug("wql result: #{output.inspect}")
return output
end
rescue => e
raise_winrm_exception(e, :wql, query)
end
def raise_winrm_exception(winrm_exception, shell, command)
# If the error is a 401, we can return a more specific error message
if winrm_exception.message.include?("401")
raise Errors::AuthError,
:user => @username,
:password => @password,
:endpoint => endpoint,
:message => winrm_exception.message
end
raise Errors::ExecutionError,
:shell => shell,
:command => command,
:message => winrm_exception.message
end
def new_session
@logger.info("Attempting to connect to WinRM...")
@logger.info(" - Host: #{@host}")
@logger.info(" - Port: #{@port}")
@logger.info(" - Username: #{@username}")
client = ::WinRM::WinRMWebService.new(endpoint, :plaintext, endpoint_options)
client.set_timeout(@timeout_in_seconds)
client.toggle_nori_type_casting(:off) #we don't want coersion of types
client
end
def session
@session ||= new_session
end
def endpoint
"http://#{@host}:#{@port}/wsman"
end
def endpoint_options
{ :user => @username,
:pass => @password,
:host => @host,
:port => @port,
:operation_timeout => @timeout_in_seconds,
:basic_auth_only => true }
end
end #WinShell class
end
end

View File

@ -0,0 +1,22 @@
en:
vagrant_winrm:
errors:
auth_error: |-
An authorization error occurred while connecting to WinRM.
User: %{user}
Password: %{password}
Endpoint: %{endpoint}
Message: %{message}
execution_error: |-
An error occurred executing a remote WinRM command.
Shell: %{shell}
Command: %{command}
Message: %{message}
invalid_shell: |-
%{shell} is not a supported type of Windows shell.
winrm_not_ready: |-
The box is not able to report an address for WinRM to connect to yet.
WinRM cannot access this Vagrant environment. Please wait for the
Vagrant environment to be running and try again.

View File

@ -24,6 +24,7 @@ Gem::Specification.new do |s|
s.add_dependency "net-scp", "~> 1.1.0"
s.add_dependency "rb-kqueue", "~> 0.2.0"
s.add_dependency "wdm", "~> 0.1.0"
s.add_dependency "winrm", "~> 1.1.3"
s.add_development_dependency "rake"
s.add_development_dependency "contest", ">= 0.1.2"