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 connection = nil begin # These are the exceptions that we retry because they represent # errors that are generally fixed from a retry and don't # necessarily represent immediate failure cases. exceptions = [ Errno::ECONNREFUSED, Errno::EHOSTUNREACH, Net::SSH::Disconnect, Timeout::Error ] @logger.info("Connecting to SSH: #{ssh_info[:host]}:#{ssh_info[:port]}") connection = retryable(:tries => @vm.config.ssh.max_tries, :on => exceptions) do Net::SSH.start(ssh_info[:host], ssh_info[:username], opts) end 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 rescue NotImplementedError # This is raised if a private key type that Net-SSH doesn't support # is used. Show a nicer error. raise Errors::SSHKeyTypeNotSupported 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? 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