vagrant/plugins/communicators/winrm/communicator.rb
Chris Roberts ea25996b21
Update Vagrant behavior outside of installers
Remove customized require behaviors and modify the bin executable
to check for missing tools that Vagrant expects to exist when
running outside of an installer.
2025-04-02 11:40:17 -07:00

230 lines
7.3 KiB
Ruby

# Copyright (c) HashiCorp, Inc.
# SPDX-License-Identifier: BUSL-1.1
require "log4r"
require "tempfile"
require "timeout"
require_relative "helper"
require_relative "shell"
require_relative "command_filter"
module VagrantPlugins
module CommunicatorWinRM
# Provides communication channel for Vagrant commands via WinRM.
class Communicator < Vagrant.plugin("2", :communicator)
include Vagrant::Util
def self.match?(machine)
# This is useless, and will likely be removed in the future (this
# whole method).
true
end
def initialize(machine)
@cmd_filter = CommandFilter.new()
@logger = Log4r::Logger.new("vagrant::communication::winrm")
@machine = machine
@shell = nil
@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 = nil
begin
winrm_info = Helper.winrm_info(@machine)
rescue Errors::WinRMNotReady
@logger.debug("WinRM not ready yet; retrying until boot_timeout is reached.")
end
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 execution_time_limit: #{shell.execution_time_limit}")
@machine.ui.detail("WinRM transport: #{shell.config.transport}")
last_message = nil
last_message_repeat_at = 0
while true
message = nil
begin
begin
return true if ready?
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 retryable 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...")
result = Timeout.timeout(@machine.config.winrm.timeout) do
shell(true).cmd("hostname")
end
@logger.info("WinRM is ready!")
return true
rescue Errors::TransientError, VagrantPlugins::CommunicatorWinRM::Errors::WinRMNotReady => 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 winrm_info was not refreshing
# thus never getting the correct hostname.
@shell = nil
return false
end
def reset!
shell(true)
end
def shell(reload=false)
@shell = nil if reload
@shell ||= create_shell
end
def execute(command, opts={}, &block)
# If this is a *nix command with no Windows equivalent, don't run it
command = @cmd_filter.filter(command)
return 0 if command.empty?
opts = {
command: command,
elevated: false,
error_check: true,
error_class: Errors::WinRMBadExitStatus,
error_key: nil, # use the error_class message key
good_exit: 0,
shell: :powershell,
interactive: false,
}.merge(opts || {})
opts[:shell] = :elevated if opts[:elevated]
opts[:good_exit] = Array(opts[:good_exit])
@logger.debug("#{opts[:shell]} executing:\n#{command}")
output = shell.send(opts[:shell], command, opts, &block)
execution_output(output, opts)
end
alias_method :sudo, :execute
def test(command, opts=nil)
# If this is a *nix command (which we know about) with no Windows
# equivalent, assume failure
command = @cmd_filter.filter(command)
return false if command.empty?
opts = {
command: command,
elevated: false,
error_check: false,
}.merge(opts || {})
# If we're passed a *nix command which PS can't parse we get exit code
# 0, but output in stderr. We need to check both exit code and stderr.
output = shell.send(:powershell, command)
return output.exitcode == 0 && output.stderr.length == 0
end
def upload(from, to)
@logger.info("Uploading: #{from} to #{to}")
shell.upload(from, to)
end
def download(from, to)
@logger.info("Downloading: #{from} to #{to}")
shell.download(from, to)
end
protected
# This creates a new WinRMShell based on the information we know
# about this machine.
def create_shell
winrm_info = Helper.winrm_info(@machine)
WinRMShell.new(
winrm_info[:host],
winrm_info[:port],
@machine.config.winrm
)
end
# Handles the raw WinRM shell result and converts it to a
# standard Vagrant communicator result
def execution_output(output, opts)
if opts[:shell] == :wql
return output
elsif opts[:error_check] && \
!opts[:good_exit].include?(output.exitcode)
raise_execution_error(output, opts)
end
output.exitcode
end
def raise_execution_error(output, opts)
# WinRM can return multiple stderr and stdout entries
error_opts = opts.merge(
stdout: output.stdout,
stderr: output.stderr
)
# Use a different error message key if the caller gave us one,
# otherwise use the error's default message
error_opts[:_key] = opts[:error_key] if opts[:error_key]
# Raise the error, use the type the caller gave us or the comm default
raise opts[:error_class], error_opts
end
end #WinRM class
end
end