lib/chef/provisioning/transport/ssh.rb in chef-provisioning-2.0.0 vs lib/chef/provisioning/transport/ssh.rb in chef-provisioning-2.0.1
- old
+ new
@@ -1,403 +1,403 @@
-require 'chef/provisioning/transport'
-require 'chef/log'
-require 'uri'
-require 'socket'
-require 'timeout'
-require 'net/ssh'
-require 'net/scp'
-require 'net/ssh/gateway'
-
-class Chef
-module Provisioning
- class Transport
- class SSH < Chef::Provisioning::Transport
- #
- # Create a new SSH transport.
- #
- # == Arguments
- #
- # - host: the host to connect to, e.g. '145.14.51.45'
- # - username: the username to connect with
- # - ssh_options: a list of options to Net::SSH.start
- # - options: a hash of options for the transport itself, including:
- # - :prefix: a prefix to send before each command (e.g. "sudo ")
- # - :ssh_pty_enable: set to false to disable pty (some instances don't
- # support this, most do)
- # - :ssh_gateway: the gateway to use, e.g. "jkeiser@145.14.51.45:222".
- # nil (the default) means no gateway. If the username is omitted,
- # then the default username is used instead (i.e. the user running
- # chef, or the username configured in .ssh/config).
- # - :scp_temp_dir: a directory to use as the temporary location for
- # files that are copied to the host via SCP.
- # Only used if :prefix is set. Default is '/tmp' if unspecified.
- # - global_config: an options hash that looks suspiciously similar to
- # Chef::Config, containing at least the key :log_level.
- #
- # The options are used in
- # Net::SSH.start(host, username, ssh_options)
-
- def initialize(host, username, ssh_options, options, global_config)
- @host = host
- @username = username
- @ssh_options = ssh_options
- @options = options
- @config = global_config
- @remote_forwards = ssh_options.delete(:remote_forwards) { Array.new }
- end
-
- attr_reader :host
- attr_reader :username
- attr_reader :ssh_options
- attr_reader :options
- attr_reader :config
-
- def execute(command, execute_options = {})
- Chef::Log.info("#{self.object_id} Executing #{options[:prefix]}#{command} on #{username}@#{host}")
- stdout = ''
- stderr = ''
- exitstatus = nil
- session # grab session outside timeout, it has its own timeout
-
- with_execute_timeout(execute_options) do
- @remote_forwards.each do |forward_info|
- # -R flag to openssh client allows optional :remote_host and
- # requires the other values so let's do that too.
- remote_host = forward_info.fetch(:remote_host, 'localhost')
- remote_port = forward_info.fetch(:remote_port)
- local_host = forward_info.fetch(:local_host)
- local_port = forward_info.fetch(:local_port)
-
- actual_port, actual_host = forward_port(local_port, local_host, remote_port, remote_host)
- Chef::Log.info("#{host} forwarded remote #{actual_host}:#{actual_port} to local #{local_host}:#{local_port}")
- end
-
- channel = session.open_channel do |channel|
- # Enable PTY unless otherwise specified, some instances require this
- unless options[:ssh_pty_enable] == false
- channel.request_pty do |chan, success|
- raise "could not get pty" if !success && options[:ssh_pty_enable]
- end
- end
-
- channel.exec("#{options[:prefix]}#{command}") do |ch, success|
- raise "could not execute command: #{command.inspect}" unless success
-
- channel.on_data do |ch2, data|
- stdout << data
- stream_chunk(execute_options, data, nil)
- end
-
- channel.on_extended_data do |ch2, type, data|
- stderr << data
- stream_chunk(execute_options, nil, data)
- end
-
- channel.on_request "exit-status" do |ch, data|
- exitstatus = data.read_long
- end
- end
- end
-
- channel.wait
-
- @remote_forwards.each do |forward_info|
- # -R flag to openssh client allows optional :remote_host and
- # requires the other values so let's do that too.
- remote_host = forward_info.fetch(:remote_host, 'localhost')
- remote_port = forward_info.fetch(:remote_port)
- local_host = forward_info.fetch(:local_host)
- local_port = forward_info.fetch(:local_port)
-
- session.forward.cancel_remote(remote_port, remote_host)
- session.loop { session.forward.active_remotes.include?([remote_port, remote_host]) }
-
- Chef::Log.info("#{host} canceled remote forward #{remote_host}:#{remote_port}")
- end
- end
-
- Chef::Log.info("Completed #{command} on #{username}@#{host}: exit status #{exitstatus}")
- Chef::Log.debug("Stdout was:\n#{stdout}") if stdout != '' && !options[:stream] && !options[:stream_stdout] && config[:log_level] != :debug
- Chef::Log.info("Stderr was:\n#{stderr}") if stderr != '' && !options[:stream] && !options[:stream_stderr] && config[:log_level] != :debug
- SSHResult.new(command, execute_options, stdout, stderr, exitstatus)
- end
-
- # TODO why does #read_file download it to the target host?
- def read_file(path)
- Chef::Log.debug("Reading file #{path} from #{username}@#{host}")
- result = StringIO.new
- download(path, result)
- result.string
- end
-
- def download_file(path, local_path)
- Chef::Log.debug("Downloading file #{path} from #{username}@#{host} to local #{local_path}")
- download(path, local_path)
- end
-
- def remote_tempfile(path)
- File.join(scp_temp_dir, "#{File.basename(path)}.#{Random.rand(2**32)}")
- end
-
- def write_file(path, content)
- execute("mkdir -p #{File.dirname(path)}").error!
- if options[:prefix]
- # Make a tempfile on the other side, upload to that, and sudo mv / chown / etc.
- tempfile = remote_tempfile(path)
- Chef::Log.debug("Writing #{content.length} bytes to #{tempfile} on #{username}@#{host}")
- Net::SCP.new(session).upload!(StringIO.new(content), tempfile)
- execute("mv #{tempfile} #{path}").error!
- else
- Chef::Log.debug("Writing #{content.length} bytes to #{path} on #{username}@#{host}")
- Net::SCP.new(session).upload!(StringIO.new(content), path)
- end
- end
-
- def upload_file(local_path, path)
- execute("mkdir -p #{File.dirname(path)}").error!
- if options[:prefix]
- # Make a tempfile on the other side, upload to that, and sudo mv / chown / etc.
- tempfile = remote_tempfile(path)
- Chef::Log.debug("Uploading #{local_path} to #{tempfile} on #{username}@#{host}")
- Net::SCP.new(session).upload!(local_path, tempfile)
- begin
- execute("mv #{tempfile} #{path}").error!
- rescue
- # Clean up if we were unable to move
- execute("rm #{tempfile}").error!
- end
- else
- Chef::Log.debug("Uploading #{local_path} to #{path} on #{username}@#{host}")
- Net::SCP.new(session).upload!(local_path, path)
- end
- end
-
- def make_url_available_to_remote(local_url)
- uri = URI(local_url)
- if is_local_machine(uri.host)
- port, host = forward_port(uri.port, uri.host, uri.port, 'localhost')
- if !port
- # Try harder if the port is already taken
- port, host = forward_port(uri.port, uri.host, 0, 'localhost')
- if !port
- raise "Error forwarding port: could not forward #{uri.port} or 0"
- end
- end
- uri.host = host
- uri.port = port
- Chef::Log.info("Port forwarded: local URL #{local_url} is available to #{self.host} as #{uri.to_s} for the duration of this SSH connection.")
- else
- Chef::Log.info("#{host} not forwarding non-local #{local_url}")
- end
- uri.to_s
- end
-
- def disconnect
- if @session
- begin
- Chef::Log.info("Closing SSH session on #{username}@#{host}")
- @session.close
- rescue
- ensure
- @session = nil
- end
- end
- end
-
- def available?
- timeout = ssh_options[:timeout] || 10
- execute('pwd', :timeout => timeout)
- true
- rescue Timeout::Error, Errno::EHOSTUNREACH, Errno::ENETUNREACH, Errno::EHOSTDOWN, Errno::ETIMEDOUT, Errno::ECONNREFUSED, Errno::ECONNRESET, Net::SSH::Disconnect, Net::SSH::ConnectionTimeout
- Chef::Log.debug("#{username}@#{host} unavailable: network connection failed or broke: #{$!.inspect}")
- disconnect
- false
- rescue Net::SSH::AuthenticationFailed, Net::SSH::HostKeyMismatch
- Chef::Log.debug("#{username}@#{host} unavailable: SSH authentication error: #{$!.inspect} ")
- disconnect
- false
- end
-
- protected
-
- def session
- @session ||= begin
- # Small initial connection timeout (10s) to help us fail faster when server is just dead
- ssh_start_opts = { timeout:10 }.merge(ssh_options)
- Chef::Log.debug("Opening SSH connection to #{username}@#{host} with options #{ssh_start_opts.dup.tap {
- |ssh| ssh.delete(:key_data) }.inspect}")
- begin
- if gateway? then gateway.ssh(host, username, ssh_start_opts)
- else Net::SSH.start(host, username, ssh_start_opts)
- end
- rescue Timeout::Error, Net::SSH::ConnectionTimeout
- Chef::Log.debug("Timed out connecting to SSH: #{$!}")
- raise InitialConnectTimeout.new($!)
- end
- end
- end
-
- def download(path, local_path)
- if options[:prefix]
- # Make a tempfile on the other side, upload to that, and sudo mv / chown / etc.
- tempfile = remote_tempfile(path)
- Chef::Log.debug("Downloading #{path} from #{tempfile} to #{local_path} on #{username}@#{host}")
- begin
- execute("cp #{path} #{tempfile}").error!
- execute("chown #{username} #{tempfile}").error!
- do_download tempfile, local_path
- rescue => e
- Chef::Log.error "Unable to download #{path} to #{tempfile} on #{username}@#{host} -- #{e}"
- nil
- ensure
- # Clean up afterwards
- begin
- execute("rm #{tempfile}").error!
- rescue => e
- Chef::Log.warn "Unable to clean up #{tempfile} on #{username}@#{host} -- #{e}"
- end
- end
- else
- do_download path, local_path
- end
- end
-
- def do_download(path, local_path)
- channel = Net::SCP.new(session).download(path, local_path)
- begin
- channel.wait
- Chef::Log.debug "SCP completed for: #{path} to #{local_path}"
- rescue Net::SCP::Error => e
- Chef::Log.error "Error with SCP: #{e}"
- # TODO we need a way to distinguish between "directory or file does not exist" and "SCP did not finish successfully"
- nil
- ensure
- # ensure the channel is closed
- channel.close
- channel.wait
- end
-
- nil
- end
-
- class SSHResult
- def initialize(command, options, stdout, stderr, exitstatus)
- @command = command
- @options = options
- @stdout = stdout
- @stderr = stderr
- @exitstatus = exitstatus
- end
-
- attr_reader :command
- attr_reader :options
- attr_reader :stdout
- attr_reader :stderr
- attr_reader :exitstatus
-
- def error!
- if exitstatus != 0
- # TODO stdout/stderr is already printed at info/debug level. Let's not print it twice, it's a lot.
- msg = "Error: command '#{command}' exited with code #{exitstatus}.\n"
- raise msg
- end
- end
- end
-
- class InitialConnectTimeout < Timeout::Error
- def initialize(original_error)
- super(original_error.message)
- @original_error = original_error
- end
-
- attr_reader :original_error
- end
-
- private
-
- def scp_temp_dir
- @scp_temp_dir ||= options.fetch(:scp_temp_dir, '/tmp')
- end
-
- def gateway?
- options.key?(:ssh_gateway) and ! options[:ssh_gateway].nil?
- end
-
- def gateway
- gw_user, gw_host = options[:ssh_gateway].split('@')
- # If we didn't have an '@' in the above, then the value is actually
- # the hostname, not the username.
- gw_host, gw_user = gw_user, gw_host if gw_host.nil?
- gw_host, gw_port = gw_host.split(':')
-
- ssh_start_opts = { timeout:10 }.merge(ssh_options)
- ssh_start_opts[:port] = gw_port || 22
-
- Chef::Log.debug("Opening SSH gateway to #{gw_user}@#{gw_host} with options #{ssh_start_opts.dup.tap {
- |ssh| ssh.delete(:key_data) }.inspect}")
- begin
- Net::SSH::Gateway.new(gw_host, gw_user, ssh_start_opts)
- rescue Errno::ETIMEDOUT
- Chef::Log.debug("Timed out connecting to gateway: #{$!}")
- raise InitialConnectTimeout.new($!)
- end
- end
-
- def is_local_machine(host)
- local_addrs = Socket.ip_address_list
- host_addrs = Addrinfo.getaddrinfo(host, nil)
- local_addrs.any? do |local_addr|
- host_addrs.any? do |host_addr|
- local_addr.ip_address == host_addr.ip_address
- end
- end
- end
-
- # Forwards a port over the connection, and returns the
- def forward_port(local_port, local_host, remote_port, remote_host)
- # This bit is from the documentation.
- if session.forward.respond_to?(:active_remote_destinations)
- # active_remote_destinations tells us exactly what remotes the current
- # ssh session is *actually* tracking. If multiple people share this
- # session and set up their own remotes, this will prevent us from
- # overwriting them.
-
- actual_remote_port, actual_remote_host = session.forward.active_remote_destinations[[local_port, local_host]]
- if !actual_remote_port
- Chef::Log.info("Forwarding local server #{local_host}:#{local_port} to #{username}@#{self.host}")
-
- session.forward.remote(local_port, local_host, remote_port, remote_host) do |new_remote_port, new_remote_host|
- actual_remote_host = new_remote_host
- actual_remote_port = new_remote_port || :error
- :no_exception # I'll take care of it myself, thanks
- end
- # Kick SSH until we get a response
- session.loop { !actual_remote_port }
- if actual_remote_port == :error
- return nil
- end
- end
- [ actual_remote_port, actual_remote_host ]
- else
- # If active_remote_destinations isn't on net-ssh, we stash our own list
- # of ports *we* have forwarded on the connection, and hope that we are
- # right.
- # TODO let's remove this when net-ssh 2.9.2 is old enough, and
- # bump the required net-ssh version.
-
- @forwarded_ports ||= {}
- remote_port, remote_host = @forwarded_ports[[local_port, local_host]]
- if !remote_port
- Chef::Log.debug("Forwarding local server #{local_host}:#{local_port} to #{username}@#{self.host}")
- old_active_remotes = session.forward.active_remotes
- session.forward.remote(local_port, local_host, local_port)
- session.loop { !(session.forward.active_remotes.length > old_active_remotes.length) }
- remote_port, remote_host = (session.forward.active_remotes - old_active_remotes).first
- @forwarded_ports[[local_port, local_host]] = [ remote_port, remote_host ]
- end
- [ remote_port, remote_host ]
- end
- end
- end
- end
-end
-end
+require 'chef/provisioning/transport'
+require 'chef/log'
+require 'uri'
+require 'socket'
+require 'timeout'
+require 'net/ssh'
+require 'net/scp'
+require 'net/ssh/gateway'
+
+class Chef
+module Provisioning
+ class Transport
+ class SSH < Chef::Provisioning::Transport
+ #
+ # Create a new SSH transport.
+ #
+ # == Arguments
+ #
+ # - host: the host to connect to, e.g. '145.14.51.45'
+ # - username: the username to connect with
+ # - ssh_options: a list of options to Net::SSH.start
+ # - options: a hash of options for the transport itself, including:
+ # - :prefix: a prefix to send before each command (e.g. "sudo ")
+ # - :ssh_pty_enable: set to false to disable pty (some instances don't
+ # support this, most do)
+ # - :ssh_gateway: the gateway to use, e.g. "jkeiser@145.14.51.45:222".
+ # nil (the default) means no gateway. If the username is omitted,
+ # then the default username is used instead (i.e. the user running
+ # chef, or the username configured in .ssh/config).
+ # - :scp_temp_dir: a directory to use as the temporary location for
+ # files that are copied to the host via SCP.
+ # Only used if :prefix is set. Default is '/tmp' if unspecified.
+ # - global_config: an options hash that looks suspiciously similar to
+ # Chef::Config, containing at least the key :log_level.
+ #
+ # The options are used in
+ # Net::SSH.start(host, username, ssh_options)
+
+ def initialize(host, username, ssh_options, options, global_config)
+ @host = host
+ @username = username
+ @ssh_options = ssh_options
+ @options = options
+ @config = global_config
+ @remote_forwards = ssh_options.delete(:remote_forwards) { Array.new }
+ end
+
+ attr_reader :host
+ attr_reader :username
+ attr_reader :ssh_options
+ attr_reader :options
+ attr_reader :config
+
+ def execute(command, execute_options = {})
+ Chef::Log.info("#{self.object_id} Executing #{options[:prefix]}#{command} on #{username}@#{host}")
+ stdout = ''
+ stderr = ''
+ exitstatus = nil
+ session # grab session outside timeout, it has its own timeout
+
+ with_execute_timeout(execute_options) do
+ @remote_forwards.each do |forward_info|
+ # -R flag to openssh client allows optional :remote_host and
+ # requires the other values so let's do that too.
+ remote_host = forward_info.fetch(:remote_host, 'localhost')
+ remote_port = forward_info.fetch(:remote_port)
+ local_host = forward_info.fetch(:local_host)
+ local_port = forward_info.fetch(:local_port)
+
+ actual_port, actual_host = forward_port(local_port, local_host, remote_port, remote_host)
+ Chef::Log.info("#{host} forwarded remote #{actual_host}:#{actual_port} to local #{local_host}:#{local_port}")
+ end
+
+ channel = session.open_channel do |channel|
+ # Enable PTY unless otherwise specified, some instances require this
+ unless options[:ssh_pty_enable] == false
+ channel.request_pty do |chan, success|
+ raise "could not get pty" if !success && options[:ssh_pty_enable]
+ end
+ end
+
+ channel.exec("#{options[:prefix]}#{command}") do |ch, success|
+ raise "could not execute command: #{command.inspect}" unless success
+
+ channel.on_data do |ch2, data|
+ stdout << data
+ stream_chunk(execute_options, data, nil)
+ end
+
+ channel.on_extended_data do |ch2, type, data|
+ stderr << data
+ stream_chunk(execute_options, nil, data)
+ end
+
+ channel.on_request "exit-status" do |ch, data|
+ exitstatus = data.read_long
+ end
+ end
+ end
+
+ channel.wait
+
+ @remote_forwards.each do |forward_info|
+ # -R flag to openssh client allows optional :remote_host and
+ # requires the other values so let's do that too.
+ remote_host = forward_info.fetch(:remote_host, 'localhost')
+ remote_port = forward_info.fetch(:remote_port)
+ local_host = forward_info.fetch(:local_host)
+ local_port = forward_info.fetch(:local_port)
+
+ session.forward.cancel_remote(remote_port, remote_host)
+ session.loop { session.forward.active_remotes.include?([remote_port, remote_host]) }
+
+ Chef::Log.info("#{host} canceled remote forward #{remote_host}:#{remote_port}")
+ end
+ end
+
+ Chef::Log.info("Completed #{command} on #{username}@#{host}: exit status #{exitstatus}")
+ Chef::Log.debug("Stdout was:\n#{stdout}") if stdout != '' && !options[:stream] && !options[:stream_stdout] && config[:log_level] != :debug
+ Chef::Log.info("Stderr was:\n#{stderr}") if stderr != '' && !options[:stream] && !options[:stream_stderr] && config[:log_level] != :debug
+ SSHResult.new(command, execute_options, stdout, stderr, exitstatus)
+ end
+
+ # TODO why does #read_file download it to the target host?
+ def read_file(path)
+ Chef::Log.debug("Reading file #{path} from #{username}@#{host}")
+ result = StringIO.new
+ download(path, result)
+ result.string
+ end
+
+ def download_file(path, local_path)
+ Chef::Log.debug("Downloading file #{path} from #{username}@#{host} to local #{local_path}")
+ download(path, local_path)
+ end
+
+ def remote_tempfile(path)
+ File.join(scp_temp_dir, "#{File.basename(path)}.#{Random.rand(2**32)}")
+ end
+
+ def write_file(path, content)
+ execute("mkdir -p #{File.dirname(path)}").error!
+ if options[:prefix]
+ # Make a tempfile on the other side, upload to that, and sudo mv / chown / etc.
+ tempfile = remote_tempfile(path)
+ Chef::Log.debug("Writing #{content.length} bytes to #{tempfile} on #{username}@#{host}")
+ Net::SCP.new(session).upload!(StringIO.new(content), tempfile)
+ execute("mv #{tempfile} #{path}").error!
+ else
+ Chef::Log.debug("Writing #{content.length} bytes to #{path} on #{username}@#{host}")
+ Net::SCP.new(session).upload!(StringIO.new(content), path)
+ end
+ end
+
+ def upload_file(local_path, path)
+ execute("mkdir -p #{File.dirname(path)}").error!
+ if options[:prefix]
+ # Make a tempfile on the other side, upload to that, and sudo mv / chown / etc.
+ tempfile = remote_tempfile(path)
+ Chef::Log.debug("Uploading #{local_path} to #{tempfile} on #{username}@#{host}")
+ Net::SCP.new(session).upload!(local_path, tempfile)
+ begin
+ execute("mv #{tempfile} #{path}").error!
+ rescue
+ # Clean up if we were unable to move
+ execute("rm #{tempfile}").error!
+ end
+ else
+ Chef::Log.debug("Uploading #{local_path} to #{path} on #{username}@#{host}")
+ Net::SCP.new(session).upload!(local_path, path)
+ end
+ end
+
+ def make_url_available_to_remote(local_url)
+ uri = URI(local_url)
+ if is_local_machine(uri.host)
+ port, host = forward_port(uri.port, uri.host, uri.port, 'localhost')
+ if !port
+ # Try harder if the port is already taken
+ port, host = forward_port(uri.port, uri.host, 0, 'localhost')
+ if !port
+ raise "Error forwarding port: could not forward #{uri.port} or 0"
+ end
+ end
+ uri.host = host
+ uri.port = port
+ Chef::Log.info("Port forwarded: local URL #{local_url} is available to #{self.host} as #{uri.to_s} for the duration of this SSH connection.")
+ else
+ Chef::Log.info("#{host} not forwarding non-local #{local_url}")
+ end
+ uri.to_s
+ end
+
+ def disconnect
+ if @session
+ begin
+ Chef::Log.info("Closing SSH session on #{username}@#{host}")
+ @session.close
+ rescue
+ ensure
+ @session = nil
+ end
+ end
+ end
+
+ def available?
+ timeout = ssh_options[:timeout] || 10
+ execute('pwd', :timeout => timeout)
+ true
+ rescue Timeout::Error, Errno::EHOSTUNREACH, Errno::ENETUNREACH, Errno::EHOSTDOWN, Errno::ETIMEDOUT, Errno::ECONNREFUSED, Errno::ECONNRESET, Net::SSH::Disconnect, Net::SSH::ConnectionTimeout
+ Chef::Log.debug("#{username}@#{host} unavailable: network connection failed or broke: #{$!.inspect}")
+ disconnect
+ false
+ rescue Net::SSH::AuthenticationFailed, Net::SSH::HostKeyMismatch
+ Chef::Log.debug("#{username}@#{host} unavailable: SSH authentication error: #{$!.inspect} ")
+ disconnect
+ false
+ end
+
+ protected
+
+ def session
+ @session ||= begin
+ # Small initial connection timeout (10s) to help us fail faster when server is just dead
+ ssh_start_opts = { timeout:10 }.merge(ssh_options)
+ Chef::Log.debug("Opening SSH connection to #{username}@#{host} with options #{ssh_start_opts.dup.tap {
+ |ssh| ssh.delete(:key_data) }.inspect}")
+ begin
+ if gateway? then gateway.ssh(host, username, ssh_start_opts)
+ else Net::SSH.start(host, username, ssh_start_opts)
+ end
+ rescue Timeout::Error, Net::SSH::ConnectionTimeout
+ Chef::Log.debug("Timed out connecting to SSH: #{$!}")
+ raise InitialConnectTimeout.new($!)
+ end
+ end
+ end
+
+ def download(path, local_path)
+ if options[:prefix]
+ # Make a tempfile on the other side, upload to that, and sudo mv / chown / etc.
+ tempfile = remote_tempfile(path)
+ Chef::Log.debug("Downloading #{path} from #{tempfile} to #{local_path} on #{username}@#{host}")
+ begin
+ execute("cp #{path} #{tempfile}").error!
+ execute("chown #{username} #{tempfile}").error!
+ do_download tempfile, local_path
+ rescue => e
+ Chef::Log.error "Unable to download #{path} to #{tempfile} on #{username}@#{host} -- #{e}"
+ nil
+ ensure
+ # Clean up afterwards
+ begin
+ execute("rm #{tempfile}").error!
+ rescue => e
+ Chef::Log.warn "Unable to clean up #{tempfile} on #{username}@#{host} -- #{e}"
+ end
+ end
+ else
+ do_download path, local_path
+ end
+ end
+
+ def do_download(path, local_path)
+ channel = Net::SCP.new(session).download(path, local_path)
+ begin
+ channel.wait
+ Chef::Log.debug "SCP completed for: #{path} to #{local_path}"
+ rescue Net::SCP::Error => e
+ Chef::Log.error "Error with SCP: #{e}"
+ # TODO we need a way to distinguish between "directory or file does not exist" and "SCP did not finish successfully"
+ nil
+ ensure
+ # ensure the channel is closed
+ channel.close
+ channel.wait
+ end
+
+ nil
+ end
+
+ class SSHResult
+ def initialize(command, options, stdout, stderr, exitstatus)
+ @command = command
+ @options = options
+ @stdout = stdout
+ @stderr = stderr
+ @exitstatus = exitstatus
+ end
+
+ attr_reader :command
+ attr_reader :options
+ attr_reader :stdout
+ attr_reader :stderr
+ attr_reader :exitstatus
+
+ def error!
+ if exitstatus != 0
+ # TODO stdout/stderr is already printed at info/debug level. Let's not print it twice, it's a lot.
+ msg = "Error: command '#{command}' exited with code #{exitstatus}.\n"
+ raise msg
+ end
+ end
+ end
+
+ class InitialConnectTimeout < Timeout::Error
+ def initialize(original_error)
+ super(original_error.message)
+ @original_error = original_error
+ end
+
+ attr_reader :original_error
+ end
+
+ private
+
+ def scp_temp_dir
+ @scp_temp_dir ||= options.fetch(:scp_temp_dir, '/tmp')
+ end
+
+ def gateway?
+ options.key?(:ssh_gateway) and ! options[:ssh_gateway].nil?
+ end
+
+ def gateway
+ gw_user, gw_host = options[:ssh_gateway].split('@')
+ # If we didn't have an '@' in the above, then the value is actually
+ # the hostname, not the username.
+ gw_host, gw_user = gw_user, gw_host if gw_host.nil?
+ gw_host, gw_port = gw_host.split(':')
+
+ ssh_start_opts = { timeout:10 }.merge(ssh_options)
+ ssh_start_opts[:port] = gw_port || 22
+
+ Chef::Log.debug("Opening SSH gateway to #{gw_user}@#{gw_host} with options #{ssh_start_opts.dup.tap {
+ |ssh| ssh.delete(:key_data) }.inspect}")
+ begin
+ Net::SSH::Gateway.new(gw_host, gw_user, ssh_start_opts)
+ rescue Errno::ETIMEDOUT
+ Chef::Log.debug("Timed out connecting to gateway: #{$!}")
+ raise InitialConnectTimeout.new($!)
+ end
+ end
+
+ def is_local_machine(host)
+ local_addrs = Socket.ip_address_list
+ host_addrs = Addrinfo.getaddrinfo(host, nil)
+ local_addrs.any? do |local_addr|
+ host_addrs.any? do |host_addr|
+ local_addr.ip_address == host_addr.ip_address
+ end
+ end
+ end
+
+ # Forwards a port over the connection, and returns the
+ def forward_port(local_port, local_host, remote_port, remote_host)
+ # This bit is from the documentation.
+ if session.forward.respond_to?(:active_remote_destinations)
+ # active_remote_destinations tells us exactly what remotes the current
+ # ssh session is *actually* tracking. If multiple people share this
+ # session and set up their own remotes, this will prevent us from
+ # overwriting them.
+
+ actual_remote_port, actual_remote_host = session.forward.active_remote_destinations[[local_port, local_host]]
+ if !actual_remote_port
+ Chef::Log.info("Forwarding local server #{local_host}:#{local_port} to #{username}@#{self.host}")
+
+ session.forward.remote(local_port, local_host, remote_port, remote_host) do |new_remote_port, new_remote_host|
+ actual_remote_host = new_remote_host
+ actual_remote_port = new_remote_port || :error
+ :no_exception # I'll take care of it myself, thanks
+ end
+ # Kick SSH until we get a response
+ session.loop { !actual_remote_port }
+ if actual_remote_port == :error
+ return nil
+ end
+ end
+ [ actual_remote_port, actual_remote_host ]
+ else
+ # If active_remote_destinations isn't on net-ssh, we stash our own list
+ # of ports *we* have forwarded on the connection, and hope that we are
+ # right.
+ # TODO let's remove this when net-ssh 2.9.2 is old enough, and
+ # bump the required net-ssh version.
+
+ @forwarded_ports ||= {}
+ remote_port, remote_host = @forwarded_ports[[local_port, local_host]]
+ if !remote_port
+ Chef::Log.debug("Forwarding local server #{local_host}:#{local_port} to #{username}@#{self.host}")
+ old_active_remotes = session.forward.active_remotes
+ session.forward.remote(local_port, local_host, local_port)
+ session.loop { !(session.forward.active_remotes.length > old_active_remotes.length) }
+ remote_port, remote_host = (session.forward.active_remotes - old_active_remotes).first
+ @forwarded_ports[[local_port, local_host]] = [ remote_port, remote_host ]
+ end
+ [ remote_port, remote_host ]
+ end
+ end
+ end
+ end
+end
+end