Mitchell Hashimoto 3a5f0cefb3 Remove ANSI escape codes from SSH output
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.
2012-01-23 19:29:07 -08:00

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