communicators/winrm: initial import from vagrant-windows
This commit is contained in:
parent
f6a5e20688
commit
2a8a04ebb9
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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.
|
|
@ -24,6 +24,7 @@ Gem::Specification.new do |s|
|
||||||
s.add_dependency "net-scp", "~> 1.1.0"
|
s.add_dependency "net-scp", "~> 1.1.0"
|
||||||
s.add_dependency "rb-kqueue", "~> 0.2.0"
|
s.add_dependency "rb-kqueue", "~> 0.2.0"
|
||||||
s.add_dependency "wdm", "~> 0.1.0"
|
s.add_dependency "wdm", "~> 0.1.0"
|
||||||
|
s.add_dependency "winrm", "~> 1.1.3"
|
||||||
|
|
||||||
s.add_development_dependency "rake"
|
s.add_development_dependency "rake"
|
||||||
s.add_development_dependency "contest", ">= 0.1.2"
|
s.add_development_dependency "contest", ">= 0.1.2"
|
||||||
|
|
Loading…
Reference in New Issue