lib/chef_metal/transport/ssh.rb in chef-metal-0.14.2 vs lib/chef_metal/transport/ssh.rb in chef-metal-0.15
- old
+ new
@@ -1,288 +2 @@
-require 'chef_metal/transport'
-require 'chef/log'
-require 'uri'
-require 'socket'
-require 'timeout'
-require 'net/ssh'
-require 'net/scp'
-require 'net/ssh/gateway'
-
-module ChefMetal
- class Transport
- class SSH < ChefMetal::Transport
- def initialize(host, username, ssh_options, options, global_config)
- @host = host
- @username = username
- @ssh_options = ssh_options
- @options = options
- @config = global_config
- 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("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
- 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
- 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
-
- 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 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.
- remote_tempfile = "/tmp/#{File.basename(path)}.#{Random.rand(2**32)}"
- Chef::Log.debug("Writing #{content.length} bytes to #{remote_tempfile} on #{username}@#{host}")
- Net::SCP.new(session).upload!(StringIO.new(content), remote_tempfile)
- execute("mv #{remote_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.
- remote_tempfile = "/tmp/#{File.basename(path)}.#{Random.rand(2**32)}"
- Chef::Log.debug("Uploading #{local_path} to #{remote_tempfile} on #{username}@#{host}")
- Net::SCP.new(session).upload!(local_path, remote_tempfile)
- execute("mv #{remote_tempfile} #{path}").error!
- 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
- end
- 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.")
- uri.to_s
- end
-
- def disconnect
- if @session
- begin
- Chef::Log.debug("Closing SSH session on #{username}@#{host}")
- @session.close
- rescue
- ensure
- @session = nil
- end
- end
- end
-
- def available?
- # If you can't pwd within 10 seconds, you can't pwd
- execute('pwd', :timeout => 10)
- true
- rescue Timeout::Error, Errno::EHOSTUNREACH, Errno::ENETUNREACH, Errno::EHOSTDOWN, Errno::ETIMEDOUT, Errno::ECONNREFUSED, Errno::ECONNRESET, Net::SSH::Disconnect
- 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
- ssh_start_opts = { timeout:10 }.merge(ssh_options)
- Chef::Log.debug("Opening SSH connection to #{username}@#{host} with options #{ssh_start_opts.inspect}")
- # Small initial connection timeout (10s) to help us fail faster when server is just dead
- begin
- if gateway? then gateway.ssh(host, username, ssh_start_opts)
- else Net::SSH.start(host, username, ssh_start_opts)
- end
- rescue Timeout::Error
- Chef::Log.debug("Timed out connecting to SSH: #{$!}")
- raise InitialConnectTimeout.new($!)
- end
- end
- end
-
- def download(path, local_path)
- channel = Net::SCP.new(session).download(path, local_path)
- begin
- channel.wait
- rescue Net::SCP::Error => e
- # TODO we need a way to distinguish between "directory of file does not exist" and "SCP did not finish successfully"
- nil
- # ensure the channel is closed when a rescue happens above
- ensure
- 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 gateway?
- options.key?(:ssh_gateway) and ! options[:ssh_gateway].nil?
- end
-
- def gateway
- gw_host, gw_user = options[:ssh_gateway].split('@').reverse
- gw_host, gw_port = gw_host.split(':')
- gw_user = ssh_options[:ssh_username] unless gw_user
-
- 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.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)
- got_remote_port, remote_host = session.forward.active_remote_destinations[[local_port, local_host]]
- if !got_remote_port
- Chef::Log.debug("Forwarding local server #{local_host}:#{local_port} to #{username}@#{self.host}")
-
- session.forward.remote(local_port, local_host, remote_port, remote_host) do |actual_remote_port|
- got_remote_port = actual_remote_port || :error
- :no_exception # I'll take care of it myself, thanks
- end
- # Kick SSH until we get a response
- session.loop { !got_remote_port }
- if got_remote_port == :error
- return nil
- end
- end
- [ got_remote_port, remote_host ]
- else
- @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
+require "chef/provisioning/transport/ssh"