From ba7b964b1ee4bf5c0f592716c30991b88aac227b Mon Sep 17 00:00:00 2001 From: Max Lincoln Date: Mon, 8 Dec 2014 19:09:11 -0500 Subject: [PATCH] Better error handling for WinRM (using winrm v1.3.0.dev.2) --- plugins/communicators/winrm/communicator.rb | 83 +++++++++++++++++++-- plugins/communicators/winrm/errors.rb | 37 ++++++++- plugins/communicators/winrm/shell.rb | 55 ++++++++++---- templates/locales/comm_winrm.yml | 43 ++++++++++- 4 files changed, 195 insertions(+), 23 deletions(-) diff --git a/plugins/communicators/winrm/communicator.rb b/plugins/communicators/winrm/communicator.rb index 4bd6e47d4..e354df5ee 100644 --- a/plugins/communicators/winrm/communicator.rb +++ b/plugins/communicators/winrm/communicator.rb @@ -25,22 +25,95 @@ module VagrantPlugins @logger.info("Initializing WinRMCommunicator") end + def wait_for_ready(timeout) + Timeout.timeout(timeout) do + # Wait for winrm_info to be ready + winrm_info = nil + while true + winrm_info = Helper.winrm_info(@machine) + break if winrm_info + sleep 0.5 + end + + # Got it! Let the user know what we're connecting to. + @machine.ui.detail("WinRM address: #{shell.host}:#{shell.port}") + @machine.ui.detail("WinRM username: #{shell.username}") + @machine.ui.detail("WinRM transport: #{shell.config.transport}") + + last_message = nil + last_message_repeat_at = 0 + while true + message = nil + begin + begin + standard_shell = shell.cmd('echo vagrant') + powershell = shell.cmd('@PowerShell Write-Host vagrant') + + unless standard_shell[:exitcode] == 0 || powershell[:exitcode] == 0 + raise Errors::InvalidShell + end + return true if shell(true).sane? + rescue Vagrant::Errors::VagrantError => e + @logger.info("WinRM not ready: #{e.inspect}") + raise + end + rescue Errors::ConnectionTimeout + message = "Connection timeout." + rescue Errors::AuthenticationFailed + message = "Authentication failure." + rescue Errors::Disconnected + message = "Remote connection disconnect." + rescue Errors::ConnectionRefused + message = "Connection refused." + rescue Errors::ConnectionReset + message = "Connection reset." + rescue Errors::HostDown + message = "Host appears down." + rescue Errors::NoRoute + message = "Host unreachable." + rescue Errors::TransientError => e + # Any other retriable errors + message = e.message + end + + # If we have a message to show, then show it. We don't show + # repeated messages unless they've been repeating longer than + # 10 seconds. + if message + message_at = Time.now.to_f + show_message = true + if last_message == message + show_message = (message_at - last_message_repeat_at) > 10.0 + end + + if show_message + @machine.ui.detail("Warning: #{message} Retrying...") + last_message = message + last_message_repeat_at = message_at + end + end + end + end + rescue Timeout::Error + return false + end + def ready? @logger.info("Checking whether WinRM is ready...") - Timeout.timeout(@machine.config.winrm.timeout) do + result = Timeout.timeout(@machine.config.winrm.timeout) do shell(true).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. + rescue Errors::TransientError => e + # We catch a `TransientError` which would signal that something went + # that might work if we wait and retry. @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 + # This resolves a problem when using vSphere where the winrm_info was not refreshing # thus never getting the correct hostname. @shell = nil return false diff --git a/plugins/communicators/winrm/errors.rb b/plugins/communicators/winrm/errors.rb index 90c3c6dda..6a5df1be1 100644 --- a/plugins/communicators/winrm/errors.rb +++ b/plugins/communicators/winrm/errors.rb @@ -6,8 +6,11 @@ module VagrantPlugins error_namespace("vagrant_winrm.errors") end - class AuthError < WinRMError - error_key(:auth_error) + class TransientError < WinRMError + end + + class AuthenticationFailed < WinRMError + error_key(:authentication_failed) end class ExecutionError < WinRMError @@ -30,9 +33,37 @@ module VagrantPlugins error_key(:winrm_file_transfer_error) end - class WinRMInvalidTransport < WinRMError + class InvalidTransport < WinRMError error_key(:invalid_transport) end + + class SSLError < WinRMError + error_key(:ssl_error) + end + + class ConnectionTimeout < TransientError + error_key(:connection_timeout) + end + + class Disconnected < TransientError + error_key(:disconnected) + end + + class ConnectionRefused < TransientError + error_key(:connection_refused) + end + + class ConnectionReset < TransientError + error_key(:connection_reset) + end + + class HostDown < TransientError + error_key(:host_down) + end + + class NoRoute < TransientError + error_key(:no_route) + end end end end diff --git a/plugins/communicators/winrm/shell.rb b/plugins/communicators/winrm/shell.rb index 2114e944e..3bdd869db 100644 --- a/plugins/communicators/winrm/shell.rb +++ b/plugins/communicators/winrm/shell.rb @@ -96,20 +96,49 @@ module VagrantPlugins end 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: @config.username, - password: @config.password, - endpoint: endpoint, - message: winrm_exception.message - end + def raise_winrm_exception(exception, shell = nil, command = nil) + case exception + when WinRM::WinRMAuthorizationError + raise Errors::AuthenticationFailed, + user: @config.username, + password: @config.password, + endpoint: endpoint, + message: exception.message + when WinRM::WinRMHTTPTransportError + case exception.response_code + # If the error is a 401, we can return a more specific error message + when 401 + raise Errors::AuthenticationFailed, + user: @config.username, + password: @config.password, + endpoint: endpoint, + message: exception.message + end - raise Errors::ExecutionError, - shell: shell, - command: command, - message: winrm_exception.message + raise Errors::ExecutionError, + shell: shell, + command: command, + message: exception.message + when OpenSSL::SSL::SSLError + raise Errors::SSLError, message: exception.message + when HTTPClient::TimeoutError + raise Errors::ConnectionTimeout, message: exception.message + when Errno::ECONNREFUSED + # This is raised if we failed to connect the max amount of times + raise Errors::ConnectionRefused + when Errno::ECONNRESET + # This is raised if we failed to connect the max number of times + # due to an ECONNRESET. + raise Errors::ConnectionReset + when Errno::EHOSTDOWN + # This is raised if we get an ICMP DestinationUnknown error. + raise Errors::HostDown + when Errno::EHOSTUNREACH + # This is raised if we can't work out how to route traffic. + raise Errors::NoRoute + else + raise + end end def new_session diff --git a/templates/locales/comm_winrm.yml b/templates/locales/comm_winrm.yml index 8800fbbb0..0f01a2755 100644 --- a/templates/locales/comm_winrm.yml +++ b/templates/locales/comm_winrm.yml @@ -1,11 +1,10 @@ en: vagrant_winrm: errors: - auth_error: |- + authentication_failed: |- An authorization error occurred while connecting to WinRM. User: %{user} - Password: %{password} Endpoint: %{endpoint} Message: %{message} winrm_bad_exit_status: |- @@ -31,6 +30,13 @@ en: %{shell} is not a supported type of Windows shell. invalid_transport: |- %{transport} is not a supported WinRM transport. + ssl_error: |- + An SSL error occurred while connecting to WinRM. This usually + occurs when you are using a self-signed certificate and have + not set the WinRM `ssl_peer_verification` config setting to false. + + Message: %{message} + 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 @@ -41,3 +47,36 @@ en: From: %{from} To: %{to} Message: %{message} + + connection_refused: |- + WinRM connection was refused! This usually happens if the VM failed to + boot properly. Some steps to try to fix this: First, try reloading your + VM with `vagrant reload`, since a simple restart sometimes fixes things. + If that doesn't work, destroy your VM and recreate it with a `vagrant destroy` + followed by a `vagrant up`. If that doesn't work, contact a Vagrant + maintainer (support channels listed on the website) for more assistance. + connection_reset: |- + WinRM connection was reset! This usually happens when the machine is + taking too long to reboot. First, try reloading your machine with + `vagrant reload`, since a simple restart sometimes fixes things. + If that doesn't work, destroy your machine and recreate it with + a `vagrant destroy` followed by a `vagrant up`. If that doesn't work, + contact support. + connection_timeout: |- + Vagrant timed out while attempting to connect via WinRM. This usually + means that the VM booted, but there are issues with the WinRM configuration + or network connectivity issues. Please try to `vagrant reload` or + `vagrant up` again. + disconnected: |- + The WinRM connection was unexpectedly closed by the remote end. This + usually indicates that WinRM within the guest machine was unable to + properly start up. Please boot the VM in GUI mode to check whether + it is booting properly. + no_route: |- + While attempting to connect with WinRM, a "no route to host" (EHOSTUNREACH) + error was received. Please verify your network settings are correct + and try again. + host_down: |- + While attempting to connect with WinRM, a "host is down" (EHOSTDOWN) + error was received. Please verify your WinRM settings are correct + and try again.