Skip to content

Commit

Permalink
Make the server definition itself available to SSH channels, rather t…
Browse files Browse the repository at this point in the history
…hat just the host name. Identify servers by their complete credentials in logs, rather than simply by hostname.

git-svn-id: http://svn.rubyonrails.org/rails/tools/capistrano@6706 5ecf4fe2-1ee6-0310-87b1-e25e094e27de
  • Loading branch information
jamis committed May 9, 2007
1 parent e44dcd3 commit 4b912ae
Show file tree
Hide file tree
Showing 17 changed files with 97 additions and 64 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG
@@ -1,5 +1,9 @@
*SVN*

* Make the server definition itself available to SSH channels, rather that just the host name [Jamis Buck]

* Identify servers by their complete credentials in logs, rather than simply by hostname [Jamis Buck]

* Uniquely identify servers based on hostname, port, and username, instead of merely on hostname [Jamis Buck]

* Allow (e.g.) scm_command and local_scm_command to be set in the event of different paths to the scm command on local vs. remote hosts. [Jamis Buck]
Expand Down
11 changes: 7 additions & 4 deletions lib/capistrano/command.rb
Expand Up @@ -52,7 +52,7 @@ def process!
logger.trace "command finished" if logger

if (failed = @channels.select { |ch| ch[:status] != 0 }).any?
hosts = failed.map { |ch| ch[:host] }
hosts = failed.map { |ch| ch[:server] }
error = CommandError.new("command #{command.inspect} failed on #{hosts.join(',')}")
error.hosts = hosts
raise error
Expand All @@ -78,12 +78,15 @@ def logger
def open_channels
sessions.map do |session|
session.open_channel do |channel|
channel[:host] = session.real_host
server = session.xserver

channel[:server] = server
channel[:host] = server.host
channel[:options] = options
channel.request_pty :want_reply => true

channel.on_success do |ch|
logger.trace "executing command", ch[:host] if logger
logger.trace "executing command", ch[:server] if logger
ch.exec(replace_placeholders(command, ch))
ch.send_data(options[:data]) if options[:data]
end
Expand All @@ -92,7 +95,7 @@ def open_channels
# just log it, don't actually raise an exception, since the
# process method will see that the status is not zero and will
# raise an exception then.
logger.important "could not open channel", ch[:host] if logger
logger.important "could not open channel", ch[:server] if logger
ch.close
end

Expand Down
2 changes: 1 addition & 1 deletion lib/capistrano/configuration/actions/inspect.rb
Expand Up @@ -22,7 +22,7 @@ module Inspect
def stream(command, options={})
invoke_command(command, options) do |ch, stream, out|
puts out if stream == :out
warn "[err :: #{ch[:host]}] #{out}" if stream == :err
warn "[err :: #{ch[:server]}] #{out}" if stream == :err
end
end

Expand Down
8 changes: 4 additions & 4 deletions lib/capistrano/configuration/actions/invocation.rb
Expand Up @@ -9,7 +9,7 @@ def self.included(base) #:nodoc:

base.default_io_proc = Proc.new do |ch, stream, out|
level = stream == :err ? :important : :info
ch[:options][:logger].send(level, out, "#{stream} :: #{ch[:host]}")
ch[:options][:logger].send(level, out, "#{stream} :: #{ch[:server]}")
end
end

Expand Down Expand Up @@ -78,9 +78,9 @@ def sudo_behavior_callback(fallback) #:nodoc:
if out =~ /password:/i
ch.send_data "#{self[:password]}\n"
elsif out =~ /try again/
if prompt_host.nil? || prompt_host == ch[:host]
prompt_host = ch[:host]
logger.important out, "#{stream} :: #{ch[:host]}"
if prompt_host.nil? || prompt_host == ch[:server]
prompt_host = ch[:server]
logger.important out, "#{stream} :: #{ch[:server]}"
reset! :password
end
else
Expand Down
10 changes: 5 additions & 5 deletions lib/capistrano/gateway.rb
Expand Up @@ -51,7 +51,7 @@ def initialize(server, options={}) #:nodoc:

mutex.synchronize do
@thread = Thread.new do
logger.trace "starting connection to gateway `#{server.host}'" if logger
logger.trace "starting connection to gateway `#{server}'" if logger
SSH.connect(server, @options) do |@session|
logger.trace "gateway connection established" if logger
mutex.synchronize { waiter.signal }
Expand Down Expand Up @@ -84,16 +84,16 @@ def shutdown!
# Net::SSH connection via that port.
def connect_to(server)
connection = nil
logger.debug "establishing connection to `#{server.host}' via gateway" if logger
logger.debug "establishing connection to `#{server}' via gateway" if logger
local_port = next_port

thread = Thread.new do
begin
local_host = ServerDefinition.new("127.0.0.1", :user => server.user, :port => local_port)
session.forward.local(local_port, server.host, server.port || 22)
connection = SSH.connect(local_host, @options)
connection.real_host = server.host
logger.trace "connected: `#{server.host}' (via gateway)" if logger
connection.xserver = server
logger.trace "connected: `#{server}' (via gateway)" if logger
rescue Errno::EADDRINUSE
local_port = next_port
retry
Expand All @@ -104,7 +104,7 @@ def connect_to(server)
end

thread.join
connection or raise ConnectionError, "could not establish connection to `#{server.host}'"
connection or raise ConnectionError, "could not establish connection to `#{server}'"
end

private
Expand Down
2 changes: 1 addition & 1 deletion lib/capistrano/recipes/deploy/remote_dependency.rb
Expand Up @@ -54,7 +54,7 @@ def message
def try(command, options)
return unless @success # short-circuit evaluation
configuration.run(command, options) do |ch,stream,out|
warn "#{ch[:host]}: #{out}" if stream == :err
warn "#{ch[:server]}: #{out}" if stream == :err
end
rescue Capistrano::CommandError => e
@success = false
Expand Down
9 changes: 9 additions & 0 deletions lib/capistrano/server_definition.rb
Expand Up @@ -38,5 +38,14 @@ def eql?(server)
def hash
@hash ||= [host, user, port].hash
end

def to_s
@to_s ||= begin
s = host
s = "#{user}@#{s}" if user
s = "#{s}:#{port}" if port && port != 22
s
end
end
end
end
6 changes: 3 additions & 3 deletions lib/capistrano/shell.rb
Expand Up @@ -131,7 +131,7 @@ def connect(task)
servers = configuration.find_servers_for_task(task)
needing_connections = servers - configuration.sessions.keys
unless needing_connections.empty?
puts "[establishing connection(s) to #{needing_connections.map { |s| s.host }.join(', ')}]"
puts "[establishing connection(s) to #{needing_connections.join(', ')}]"
configuration.establish_connections_to(needing_connections)
end
servers
Expand Down Expand Up @@ -173,10 +173,10 @@ def exec_command(command, servers)
if out =~ /Password:\s*/i
ch.send_data "#{configuration[:password]}\n"
else
puts "[#{ch[:host]}] #{line.chomp}"
puts "[#{ch[:server]}] #{line.chomp}"
end
elsif stream == :err
puts "[#{ch[:host]} ERR] #{line.chomp}"
puts "[#{ch[:server]} ERR] #{line.chomp}"
end
end
end
Expand Down
20 changes: 10 additions & 10 deletions lib/capistrano/ssh.rb
Expand Up @@ -12,18 +12,18 @@ module Capistrano

# A helper class for dealing with SSH connections.
class SSH
# Patch an accessor onto an SSH connection so that we can record the "real"
# host behind the connection. This is useful because the gateway returns
# connections whose "host" is 127.0.0.1, instead of the host on the other
# side of the tunnel.
module RealHost #:nodoc:
def self.apply_to(connection, host)
connection.extend(RealHost)
connection.real_host = host
# Patch an accessor onto an SSH connection so that we can record the server
# definition object that defines the connection. This is useful because
# the gateway returns connections whose "host" is 127.0.0.1, instead of
# the host on the other side of the tunnel.
module Server #:nodoc:
def self.apply_to(connection, server)
connection.extend(Server)
connection.xserver = server
connection
end

attr_accessor :real_host
attr_accessor :xserver
end

# The default port for SSH.
Expand All @@ -49,7 +49,7 @@ def self.connect(server, options={}, &block)
ssh_options.update(options[:ssh_options]) if options[:ssh_options]

connection = Net::SSH.start(server.host, ssh_options, &block)
RealHost.apply_to(connection, server.host)
Server.apply_to(connection, server)

rescue Net::SSH::AuthenticationFailed
raise if methods.empty?
Expand Down
10 changes: 5 additions & 5 deletions lib/capistrano/upload.rb
Expand Up @@ -80,19 +80,19 @@ def logger

def setup_sftp
sessions.map do |session|
host = session.host
server = session.xserver
sftp = session.sftp
sftp.connect unless sftp.state == :open

sftp.channel[:done] = false
sftp.open(filename, IO::WRONLY | IO::CREAT | IO::TRUNC, options[:mode] || 0660) do |status, handle|
break unless check_status(sftp, "open #{filename}", host, status)
break unless check_status(sftp, "open #{filename}", server, status)

logger.info "uploading data to #{host}:#{filename}" if logger
logger.info "uploading data to #{server}:#{filename}" if logger
sftp.write(handle, options[:data] || "") do |status|
break unless check_status(sftp, "write to #{host}:#{filename}", host, status)
break unless check_status(sftp, "write to #{server}:#{filename}", server, status)
sftp.close_handle(handle) do
logger.debug "done uploading data to #{host}:#{filename}" if logger
logger.debug "done uploading data to #{server}:#{filename}" if logger
completed!(sftp)
end
end
Expand Down
15 changes: 9 additions & 6 deletions test/command_test.rb
Expand Up @@ -53,7 +53,7 @@ def test_env_with_multiple_keys_should_chain_the_entries_together
end

def test_open_channel_should_set_host_key_on_channel
session = mock(:real_host => "capistrano")
session = mock(:xserver => server("capistrano"))
channel = stub_everything

session.expects(:open_channel).yields(channel)
Expand All @@ -63,7 +63,7 @@ def test_open_channel_should_set_host_key_on_channel
end

def test_open_channel_should_set_options_key_on_channel
session = mock(:real_host => "capistrano")
session = mock(:xserver => server("capistrano"))
channel = stub_everything

session.expects(:open_channel).yields(channel)
Expand All @@ -73,7 +73,7 @@ def test_open_channel_should_set_options_key_on_channel
end

def test_open_channel_should_request_pty
session = mock(:real_host => "capistrano")
session = mock(:xserver => server("capistrano"))
channel = stub_everything

session.expects(:open_channel).yields(channel)
Expand Down Expand Up @@ -177,7 +177,7 @@ def test_command_error_should_include_accessor_with_host_array
flunk "expected an exception to be raised"
rescue Capistrano::CommandError => e
assert e.respond_to?(:hosts)
assert_equal %w(capistrano), e.hosts
assert_equal %w(capistrano), e.hosts.map { |h| h.to_s }
end
end

Expand Down Expand Up @@ -251,17 +251,20 @@ def new_channel(closed, status=nil)
ch.expects(:[]).with(:status).returns(status) if status
ch.expects(:close) unless closed
ch.stubs(:[]).with(:host).returns("capistrano")
ch.stubs(:[]).with(:server).returns(server("capistrano"))
ch
end

def setup_for_extracting_channel_action(action, *args)
session = mock(:real_host => "capistrano")
s = server("capistrano")
session = mock("session", :xserver => s)

channel = stub_everything
session.expects(:open_channel).yields(channel)

ch = mock
ch.stubs(:[]).with(:host).returns("capistrano")
ch.stubs(:[]).with(:server).returns(s)
ch.stubs(:[]).with(:host).returns(s.host)
channel.expects(action).yields(ch, *args)

yield ch if block_given?
Expand Down
4 changes: 2 additions & 2 deletions test/configuration/actions/inspect_test.rb
@@ -1,7 +1,7 @@
require "#{File.dirname(__FILE__)}/../../utils"
require 'capistrano/configuration/actions/inspect'

class ConfigurationActionsRunTest < Test::Unit::TestCase
class ConfigurationActionsInspectTest < Test::Unit::TestCase
class MockConfig
include Capistrano::Configuration::Actions::Inspect
end
Expand All @@ -25,7 +25,7 @@ def test_stream_should_emit_stdout_via_puts

def test_stream_should_emit_stderr_via_warn
ch = mock("channel")
ch.expects(:[]).with(:host).returns("capistrano")
ch.expects(:[]).with(:server).returns(server("capistrano"))
@config.expects(:invoke_command).yields(ch, :err, "something streamed")
@config.expects(:puts).never
@config.expects(:warn).with("[err :: capistrano] something streamed")
Expand Down
7 changes: 6 additions & 1 deletion test/configuration/actions/invocation_test.rb
Expand Up @@ -36,7 +36,7 @@ def test_run_options_should_be_passed_to_execute_on_servers
end

def test_run_without_block_should_use_default_io_proc
@config.expects(:execute_on_servers).yields(%w(s1 s2 s3).map { |s| mock(:host => s) })
@config.expects(:execute_on_servers).yields(%w(s1 s2 s3).map { |s| server(s) })
@config.expects(:sessions).returns(Hash.new { |h,k| h[k] = k.host.to_sym }).times(3)
prepare_command("ls", [:s1, :s2, :s3], {:logger => @config.logger})
MockConfig.default_io_proc = inspectable_proc
Expand All @@ -53,13 +53,15 @@ def test_run_with_block_should_use_block

def test_default_io_proc_should_log_stdout_arguments_as_info
ch = { :host => "capistrano",
:server => server("capistrano"),
:options => { :logger => mock("logger") } }
ch[:options][:logger].expects(:info).with("data stuff", "out :: capistrano")
MockConfig.default_io_proc[ch, :out, "data stuff"]
end

def test_default_io_proc_should_log_stderr_arguments_as_important
ch = { :host => "capistrano",
:server => server("capistrano"),
:options => { :logger => mock("logger") } }
ch[:options][:logger].expects(:important).with("data stuff", "err :: capistrano")
MockConfig.default_io_proc[ch, :err, "data stuff"]
Expand Down Expand Up @@ -103,6 +105,7 @@ def test_sudo_behavior_callback_should_send_password_when_prompted_with_SuSE_dia
def test_sudo_behavior_callback_with_incorrect_password_on_first_prompt
ch = mock("channel")
ch.stubs(:[]).with(:host).returns("capistrano")
ch.stubs(:[]).with(:server).returns(server("capistrano"))
@config.expects(:reset!).with(:password)
@config.sudo_behavior_callback(nil)[ch, nil, "blah blah try again blah blah"]
end
Expand All @@ -112,8 +115,10 @@ def test_sudo_behavior_callback_with_incorrect_password_on_subsequent_prompts

ch = mock("channel")
ch.stubs(:[]).with(:host).returns("capistrano")
ch.stubs(:[]).with(:server).returns(server("capistrano"))
ch2 = mock("channel")
ch2.stubs(:[]).with(:host).returns("cap2")
ch2.stubs(:[]).with(:server).returns(server("cap2"))

@config.expects(:reset!).with(:password).times(2)

Expand Down

0 comments on commit 4b912ae

Please sign in to comment.