mirror of
https://github.com/hashicorp/vagrant.git
synced 2026-02-03 20:39:27 -05:00
252 lines
8.5 KiB
Ruby
252 lines
8.5 KiB
Ruby
# Copyright (c) HashiCorp, Inc.
|
|
# SPDX-License-Identifier: BUSL-1.1
|
|
|
|
require File.expand_path("../../ssh/communicator", __FILE__)
|
|
|
|
require 'net/sftp'
|
|
|
|
module VagrantPlugins
|
|
module CommunicatorWinSSH
|
|
# This class provides communication with a Windows VM running
|
|
# the Windows native port of OpenSSH
|
|
class Communicator < VagrantPlugins::CommunicatorSSH::Communicator
|
|
# Command to run when checking if connection is ready and working
|
|
READY_COMMAND="dir"
|
|
|
|
def initialize(machine)
|
|
super
|
|
@logger = Log4r::Logger.new("vagrant::communication::winssh")
|
|
end
|
|
|
|
# Wrap the shell if required. By default we are using powershell
|
|
# which requires no modification. If cmd is defined as shell, add
|
|
# prefix to start within cmd.exe
|
|
def shell_cmd(opts)
|
|
case opts[:shell].to_s
|
|
when "cmd"
|
|
"cmd.exe /c '#{opts[:command]}'"
|
|
else
|
|
opts[:command]
|
|
end
|
|
end
|
|
|
|
# Executes the command on an SSH connection within a login shell.
|
|
def shell_execute(connection, command, **opts)
|
|
opts[:shell] ||= machine_config_ssh.shell
|
|
|
|
command = shell_cmd(opts.merge(command: command))
|
|
|
|
@logger.info("Execute: #{command} - opts: #{opts}")
|
|
exit_status = nil
|
|
|
|
# Open the channel so we can execute or command
|
|
channel = connection.open_channel do |ch|
|
|
marker_found = false
|
|
data_buffer = ''
|
|
stderr_marker_found = false
|
|
stderr_data_buffer = ''
|
|
|
|
@logger.debug("Base SSH exec command: #{command}")
|
|
command = "$ProgressPreference = 'SilentlyContinue';Write-Output #{CMD_GARBAGE_MARKER};[Console]::Error.WriteLine('#{CMD_GARBAGE_MARKER}');#{command}"
|
|
|
|
ch.exec(command) do |ch2, _|
|
|
# Setup the channel callbacks so we can get data and exit status
|
|
ch2.on_data do |ch3, data|
|
|
# Filter out the clear screen command
|
|
data = remove_ansi_escape_codes(data)
|
|
|
|
if !marker_found
|
|
data_buffer << data
|
|
marker_index = data_buffer.index(CMD_GARBAGE_MARKER)
|
|
if marker_index
|
|
marker_found = true
|
|
data_buffer.slice!(0, marker_index + CMD_GARBAGE_MARKER.size)
|
|
data.replace(data_buffer)
|
|
data_buffer = nil
|
|
end
|
|
end
|
|
|
|
if block_given? && marker_found
|
|
yield :stdout, data
|
|
end
|
|
end
|
|
|
|
ch2.on_extended_data do |ch3, type, data|
|
|
# Filter out the clear screen command
|
|
data = remove_ansi_escape_codes(data)
|
|
@logger.debug("stderr: #{data}")
|
|
if !stderr_marker_found
|
|
stderr_data_buffer << data
|
|
marker_index = stderr_data_buffer.index(CMD_GARBAGE_MARKER)
|
|
if marker_index
|
|
stderr_marker_found = true
|
|
stderr_data_buffer.slice!(0, marker_index + CMD_GARBAGE_MARKER.size)
|
|
data.replace(stderr_data_buffer.lstrip)
|
|
data_buffer = nil
|
|
end
|
|
end
|
|
|
|
if block_given? && stderr_marker_found && !data.empty?
|
|
yield :stderr, data
|
|
end
|
|
end
|
|
|
|
ch2.on_request("exit-status") do |ch3, data|
|
|
exit_status = data.read_long
|
|
@logger.debug("Exit status: #{exit_status}")
|
|
|
|
# Close the channel, since after the exit status we're
|
|
# probably done. This fixes up issues with hanging.
|
|
ch.close
|
|
end
|
|
|
|
end
|
|
end
|
|
|
|
begin
|
|
keep_alive = nil
|
|
|
|
if @machine.config.ssh.keep_alive
|
|
# Begin sending keep-alive packets while we wait for the script
|
|
# to complete. This avoids connections closing on long-running
|
|
# scripts.
|
|
keep_alive = Thread.new do
|
|
loop do
|
|
sleep 5
|
|
@logger.debug("Sending SSH keep-alive...")
|
|
connection.send_global_request("keep-alive@openssh.com")
|
|
end
|
|
end
|
|
end
|
|
|
|
# Wait for the channel to complete
|
|
begin
|
|
channel.wait
|
|
rescue Errno::ECONNRESET, IOError
|
|
@logger.info(
|
|
"SSH connection unexpected closed. Assuming reboot or something.")
|
|
exit_status = 0
|
|
pty = false
|
|
rescue Net::SSH::ChannelOpenFailed
|
|
raise Vagrant::Errors::SSHChannelOpenFail
|
|
rescue Net::SSH::Disconnect
|
|
raise Vagrant::Errors::SSHDisconnected
|
|
end
|
|
ensure
|
|
# Kill the keep-alive thread
|
|
keep_alive.kill if keep_alive
|
|
end
|
|
|
|
# Return the final exit status
|
|
return exit_status
|
|
end
|
|
|
|
def machine_config_ssh
|
|
@machine.config.winssh
|
|
end
|
|
|
|
def download(from, to=nil)
|
|
@logger.debug("Downloading: #{from} to #{to}")
|
|
|
|
sftp_connect do |sftp|
|
|
sftp.download!(from, to)
|
|
end
|
|
end
|
|
|
|
# Note: I could not get Net::SFTP to throw a permissions denied error,
|
|
# even when uploading to a directory where I did not have write
|
|
# privileges. I believe this is because Windows SSH sessions are started
|
|
# in an elevated process.
|
|
def upload(from, to)
|
|
to = Vagrant::Util::Platform.unix_windows_path(to)
|
|
@logger.debug("Uploading: #{from} to #{to}")
|
|
|
|
if File.directory?(from)
|
|
if from.end_with?(".")
|
|
@logger.debug("Uploading directory contents of: #{from}")
|
|
from = from.sub(/\.$/, "")
|
|
else
|
|
@logger.debug("Uploading full directory container of: #{from}")
|
|
to = File.join(to, File.basename(File.expand_path(from)))
|
|
end
|
|
end
|
|
|
|
sftp_connect do |sftp|
|
|
uploader = lambda do |path, remote_dest=nil|
|
|
if File.directory?(path)
|
|
Dir.new(path).each do |entry|
|
|
next if entry == "." || entry == ".."
|
|
full_path = File.join(path, entry)
|
|
dest = File.join(to, path.sub(/^#{Regexp.escape(from)}/, ""))
|
|
sftp.mkdir(dest)
|
|
uploader.call(full_path, dest)
|
|
end
|
|
else
|
|
if remote_dest
|
|
dest = File.join(remote_dest, File.basename(path))
|
|
else
|
|
dest = to
|
|
if to.end_with?(File::SEPARATOR)
|
|
dest = File.join(to, File.basename(path))
|
|
end
|
|
end
|
|
@logger.debug("Ensuring remote directory exists for destination upload")
|
|
sftp.mkdir(File.dirname(dest))
|
|
@logger.debug("Uploading file #{path} to remote #{dest}")
|
|
upload_file = File.open(path, "rb")
|
|
begin
|
|
sftp.upload!(upload_file, dest)
|
|
ensure
|
|
upload_file.close
|
|
end
|
|
end
|
|
end
|
|
uploader.call(from)
|
|
end
|
|
end
|
|
|
|
# Opens an SFTP connection and yields it so that you can download and
|
|
# upload files. SFTP works more reliably than SCP on Windows due to
|
|
# issues with shell quoting and escaping.
|
|
def sftp_connect
|
|
# Connect to SFTP and yield the SFTP object
|
|
connect do |connection|
|
|
return yield connection.sftp
|
|
end
|
|
end
|
|
|
|
protected
|
|
|
|
# The WinSSH communicator connection provides isolated modification
|
|
# to the generated connection instances. This modification forces
|
|
# all provided commands to run within powershell
|
|
def connect(**opts)
|
|
connection = nil
|
|
super { |c| connection = c }
|
|
|
|
if !connection.instance_variable_get(:@winssh_patched)
|
|
open_chan = connection.method(:open_channel)
|
|
connection.define_singleton_method(:open_channel) do |*args, &chan_block|
|
|
open_chan.call(*args) do |ch|
|
|
exec = ch.method(:exec)
|
|
ch.define_singleton_method(:exec) do |command, &block|
|
|
command = Base64.strict_encode64(command.encode("UTF-16LE", "UTF-8"))
|
|
command = "powershell -NoLogo -NonInteractive -ExecutionPolicy Bypass " \
|
|
"-NoProfile -EncodedCommand #{command}"
|
|
exec.call(command, &block)
|
|
end
|
|
chan_block.call(ch)
|
|
end
|
|
end
|
|
connection.instance_variable_set(:@winssh_patched, true)
|
|
end
|
|
|
|
if block_given?
|
|
yield connection
|
|
else
|
|
connection
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|