Note that the ANSI escape code removal is not complete, but is fairly comprehensive in terms of the codes that really muck with the terminal layout.
214 lines
6.9 KiB
Ruby
214 lines
6.9 KiB
Ruby
require 'timeout'
|
|
|
|
require 'log4r'
|
|
require 'net/ssh'
|
|
require 'net/scp'
|
|
|
|
require 'vagrant/util/ansi_escape_code_remover'
|
|
require 'vagrant/util/file_mode'
|
|
require 'vagrant/util/platform'
|
|
require 'vagrant/util/retryable'
|
|
|
|
module Vagrant
|
|
module Communication
|
|
# Provides communication with the VM via SSH.
|
|
class SSH < Base
|
|
include Util::ANSIEscapeCodeRemover
|
|
include Util::Retryable
|
|
|
|
def initialize(vm)
|
|
@vm = vm
|
|
@logger = Log4r::Logger.new("vagrant::communication::ssh")
|
|
@connection = nil
|
|
end
|
|
|
|
def ready?
|
|
@logger.debug("Checking whether SSH is ready...")
|
|
|
|
Timeout.timeout(@vm.config.ssh.timeout) do
|
|
connect
|
|
end
|
|
|
|
# If we reached this point then we successfully connected
|
|
@logger.info("SSH is ready!")
|
|
true
|
|
rescue Timeout::Error, Errors::SSHConnectionRefused, Net::SSH::Disconnect => e
|
|
# The above errors represent various reasons that SSH may not be
|
|
# ready yet. Return false.
|
|
@logger.info("SSH not up: #{e.inspect}")
|
|
return false
|
|
end
|
|
|
|
def execute(command, opts=nil, &block)
|
|
opts = {
|
|
:error_check => true,
|
|
:error_class => Errors::VagrantError,
|
|
:error_key => :ssh_bad_exit_status,
|
|
:command => command,
|
|
:sudo => false
|
|
}.merge(opts || {})
|
|
|
|
# Connect via SSH and execute the command in the shell.
|
|
exit_status = connect do |connection|
|
|
shell_execute(connection, command, opts[:sudo], &block)
|
|
end
|
|
|
|
# Check for any errors
|
|
if opts[:error_check] && exit_status != 0
|
|
# 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`
|
|
error_opts = opts.merge(:_key => opts[:error_key])
|
|
raise opts[:error_class], error_opts
|
|
end
|
|
|
|
# Return the exit status
|
|
exit_status
|
|
end
|
|
|
|
def sudo(command, opts=nil, &block)
|
|
# Run `execute` but with the `sudo` option.
|
|
opts = { :sudo => true }.merge(opts || {})
|
|
execute(command, opts, &block)
|
|
end
|
|
|
|
def upload(from, to)
|
|
@logger.debug("Uploading: #{from} to #{to}")
|
|
|
|
# Do an SCP-based upload...
|
|
connect do |connection|
|
|
scp = Net::SCP.new(connection)
|
|
scp.upload!(from, to)
|
|
end
|
|
rescue Net::SCP::Error => e
|
|
# If we get the exit code of 127, then this means SCP is unavailable.
|
|
raise Errors::SCPUnavailable if e.message =~ /\(127\)/
|
|
|
|
# Otherwise, just raise the error up
|
|
raise
|
|
end
|
|
|
|
protected
|
|
|
|
# Opens an SSH connection and yields it to a block.
|
|
def connect
|
|
if @connection && !@connection.closed?
|
|
# There is a chance that the socket is closed despite us checking
|
|
# 'closed?' above. To test this we need to send data through the
|
|
# socket.
|
|
begin
|
|
@connection.exec!("")
|
|
rescue IOError
|
|
@logger.info("Connection has been closed. Not re-using.")
|
|
@connection = nil
|
|
end
|
|
|
|
# If the @connection is still around, then it is valid,
|
|
# and we use it.
|
|
if @connection
|
|
@logger.debug("Re-using SSH connection.")
|
|
return yield @connection if block_given?
|
|
return
|
|
end
|
|
end
|
|
|
|
ssh_info = @vm.ssh.info
|
|
|
|
# Build the options we'll use to initiate the connection via Net::SSH
|
|
opts = {
|
|
:port => ssh_info[:port],
|
|
:keys => [ssh_info[:private_key_path]],
|
|
:keys_only => true,
|
|
:user_known_hosts_file => [],
|
|
:paranoid => false,
|
|
:config => false,
|
|
:forward_agent => ssh_info[:forward_agent]
|
|
}
|
|
|
|
# Check that the private key permissions are valid
|
|
@vm.ssh.check_key_permissions(ssh_info[:private_key_path])
|
|
|
|
# Connect to SSH, giving it a few tries
|
|
@logger.info("Connecting to SSH: #{ssh_info[:host]}:#{ssh_info[:port]}")
|
|
exceptions = [Errno::ECONNREFUSED, Net::SSH::Disconnect]
|
|
connection = retryable(:tries => @vm.config.ssh.max_tries, :on => exceptions) do
|
|
Net::SSH.start(ssh_info[:host], ssh_info[:username], opts)
|
|
end
|
|
|
|
@connection = connection
|
|
|
|
# This is hacky but actually helps with some issues where
|
|
# Net::SSH is simply not robust enough to handle... see
|
|
# issue #391, #455, etc.
|
|
sleep 4
|
|
|
|
# Yield the connection that is ready to be used and
|
|
# return the value of the block
|
|
return yield connection if block_given?
|
|
rescue Net::SSH::AuthenticationFailed
|
|
# This happens if authentication failed. We wrap the error in our
|
|
# own exception.
|
|
raise Errors::SSHAuthenticationFailed
|
|
rescue Errno::ECONNREFUSED
|
|
# This is raised if we failed to connect the max amount of times
|
|
raise Errors::SSHConnectionRefused
|
|
end
|
|
|
|
# Executes the command on an SSH connection within a login shell.
|
|
def shell_execute(connection, command, sudo=false)
|
|
@logger.info("Execute: #{command} (sudo=#{sudo.inspect})")
|
|
exit_status = nil
|
|
|
|
# Determine the shell to execute. If we are using `sudo` then we
|
|
# need to wrap the shell in a `sudo` call.
|
|
shell = "#{@vm.config.ssh.shell} -l"
|
|
shell = "sudo -H #{shell}" if sudo
|
|
|
|
# Open the channel so we can execute or command
|
|
channel = connection.open_channel do |ch|
|
|
ch.exec(shell) do |ch2, _|
|
|
# Setup the channel callbacks so we can get data and exit status
|
|
ch2.on_data do |ch3, data|
|
|
if block_given?
|
|
# Filter out the clear screen command
|
|
data = remove_ansi_escape_codes(data)
|
|
@logger.debug("stdout: #{data}")
|
|
yield :stdout, data
|
|
end
|
|
end
|
|
|
|
ch2.on_extended_data do |ch3, type, data|
|
|
if block_given?
|
|
# Filter out the clear screen command
|
|
data = remove_ansi_escape_codes(data)
|
|
@logger.debug("stderr: #{data}")
|
|
yield :stderr, data
|
|
end
|
|
end
|
|
|
|
ch2.on_request("exit-status") do |ch3, data|
|
|
exit_status = data.read_long
|
|
@logger.debug("Exit status: #{exit_status}")
|
|
end
|
|
|
|
# Set the terminal
|
|
ch2.send_data "export TERM=vt100\n"
|
|
|
|
# Output the command
|
|
ch2.send_data "#{command}\n"
|
|
|
|
# Remember to exit or this channel will hang open
|
|
ch2.send_data "exit\n"
|
|
end
|
|
end
|
|
|
|
# Wait for the channel to complete
|
|
channel.wait
|
|
|
|
# Return the final exit status
|
|
return exit_status
|
|
end
|
|
end
|
|
end
|
|
end
|