mirror of
https://github.com/capistrano/capistrano
synced 2023-03-27 23:21:18 -04:00
Works with public keys now, for passwordless operation
git-svn-id: http://svn.rubyonrails.org/rails/trunk/switchtower@2000 5ecf4fe2-1ee6-0310-87b1-e25e094e27de
This commit is contained in:
parent
13e3179a02
commit
5246654ab7
7 changed files with 181 additions and 31 deletions
|
@ -1,5 +1,7 @@
|
|||
*SVN*
|
||||
|
||||
* Works with public keys now, for passwordless deployment
|
||||
|
||||
* Subversion module recognizes the password prompt for HTTP authentication
|
||||
|
||||
* Preserve +x on scripts when using darcs #1929 [Scott Barron]
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
require 'erb'
|
||||
require 'net/ssh'
|
||||
require 'switchtower/command'
|
||||
require 'switchtower/gateway'
|
||||
require 'switchtower/ssh'
|
||||
|
||||
module SwitchTower
|
||||
|
||||
|
@ -12,7 +12,7 @@ module SwitchTower
|
|||
# new actor via Configuration#actor.
|
||||
class Actor
|
||||
|
||||
# An adaptor for making the Net::SSH interface look and act like that of the
|
||||
# An adaptor for making the SSH interface look and act like that of the
|
||||
# Gateway class.
|
||||
class DefaultConnectionFactory #:nodoc:
|
||||
def initialize(config)
|
||||
|
@ -20,8 +20,7 @@ module SwitchTower
|
|||
end
|
||||
|
||||
def connect_to(server)
|
||||
Net::SSH.start(server, :username => @config.user,
|
||||
:password => @config.password)
|
||||
SSH.connect(server, @config)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -40,7 +39,7 @@ module SwitchTower
|
|||
# instances of Actor::Task.
|
||||
attr_reader :tasks
|
||||
|
||||
# A hash of the Net::SSH sessions that are currently open and available.
|
||||
# A hash of the SSH sessions that are currently open and available.
|
||||
# Because sessions are constructed lazily, this will only contain
|
||||
# connections to those servers that have been the targets of one or more
|
||||
# executed tasks.
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
require 'thread'
|
||||
require 'net/ssh'
|
||||
require 'switchtower/ssh'
|
||||
|
||||
Thread.abort_on_exception = true
|
||||
|
||||
|
@ -36,9 +36,7 @@ module SwitchTower
|
|||
|
||||
@thread = Thread.new do
|
||||
@config.logger.trace "starting connection to gateway #{server}"
|
||||
Net::SSH.start(server, :username => @config.user,
|
||||
:password => @config.password
|
||||
) do |@session|
|
||||
SSH.connect(server, @config) do |@session|
|
||||
@config.logger.trace "gateway connection established"
|
||||
@mutex.synchronize { waiter.signal }
|
||||
connection = @session.registry[:connection][:driver]
|
||||
|
@ -93,9 +91,8 @@ module SwitchTower
|
|||
|
||||
begin
|
||||
@session.forward.local(port, key, 22)
|
||||
@pending_forward_requests[key] =
|
||||
Net::SSH.start('127.0.0.1', :username => @config.user,
|
||||
:password => @config.password, :port => port)
|
||||
@pending_forward_requests[key] = SSH.connect('127.0.0.1', @config,
|
||||
port)
|
||||
@config.logger.trace "connection to #{key} via gateway established"
|
||||
rescue Object
|
||||
@pending_forward_requests[key] = nil
|
||||
|
|
30
lib/switchtower/ssh.rb
Normal file
30
lib/switchtower/ssh.rb
Normal file
|
@ -0,0 +1,30 @@
|
|||
require 'net/ssh'
|
||||
|
||||
module SwitchTower
|
||||
# A helper class for dealing with SSH connections.
|
||||
class SSH
|
||||
# An abstraction to make it possible to connect to the server via public key
|
||||
# without prompting for the password. If the public key authentication fails
|
||||
# this will fall back to password authentication.
|
||||
#
|
||||
# If a block is given, the new session is yielded to it, otherwise the new
|
||||
# session is returned.
|
||||
def self.connect(server, config, port=22, &block)
|
||||
methods = [ %w(publickey hostbased), %w(password keyboard-interactive) ]
|
||||
password_value = nil
|
||||
|
||||
begin
|
||||
Net::SSH.start(server,
|
||||
:username => config.user,
|
||||
:password => password_value,
|
||||
:port => port,
|
||||
:auth_methods => methods.shift,
|
||||
&block)
|
||||
rescue Net::SSH::AuthenticationFailed
|
||||
raise if methods.empty?
|
||||
password_value = config.password
|
||||
retry
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,28 +1,10 @@
|
|||
$:.unshift File.dirname(__FILE__) + "/../../lib"
|
||||
|
||||
require File.dirname(__FILE__) + "/../utils"
|
||||
require 'test/unit'
|
||||
require 'switchtower/scm/subversion'
|
||||
|
||||
class ScmSubversionTest < Test::Unit::TestCase
|
||||
class MockLogger
|
||||
def info(msg,pfx=nil) end
|
||||
def debug(msg,pfx=nil) end
|
||||
end
|
||||
|
||||
class MockConfiguration < Hash
|
||||
def logger
|
||||
@logger ||= MockLogger.new
|
||||
end
|
||||
|
||||
def method_missing(sym, *args)
|
||||
if args.length == 0
|
||||
self[sym]
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class SubversionTest < SwitchTower::SCM::Subversion
|
||||
attr_accessor :story
|
||||
attr_reader :last_path
|
||||
|
|
104
test/ssh_test.rb
Normal file
104
test/ssh_test.rb
Normal file
|
@ -0,0 +1,104 @@
|
|||
$:.unshift File.dirname(__FILE__) + "/../lib"
|
||||
|
||||
require File.dirname(__FILE__) + "/utils"
|
||||
require 'test/unit'
|
||||
require 'switchtower/ssh'
|
||||
|
||||
class SSHTest < Test::Unit::TestCase
|
||||
class MockSSH
|
||||
AuthenticationFailed = Net::SSH::AuthenticationFailed
|
||||
|
||||
class <<self
|
||||
attr_accessor :story
|
||||
attr_accessor :invocations
|
||||
end
|
||||
|
||||
def self.start(server, opts, &block)
|
||||
@invocations << [server, opts, block]
|
||||
err = story.shift
|
||||
raise err if err
|
||||
end
|
||||
end
|
||||
|
||||
def setup
|
||||
@config = MockConfiguration.new
|
||||
@config[:user] = 'demo'
|
||||
@config[:password] = 'c0c0nutfr0st1ng'
|
||||
MockSSH.story = []
|
||||
MockSSH.invocations = []
|
||||
end
|
||||
|
||||
def test_publickey_auth_succeeds_default_port_no_block
|
||||
Net.const_during(:SSH, MockSSH) do
|
||||
SwitchTower::SSH.connect('demo.server.i', @config)
|
||||
end
|
||||
|
||||
assert_equal 1, MockSSH.invocations.length
|
||||
assert_equal 'demo.server.i', MockSSH.invocations.first[0]
|
||||
assert_equal 22, MockSSH.invocations.first[1][:port]
|
||||
assert_equal 'demo', MockSSH.invocations.first[1][:username]
|
||||
assert_nil MockSSH.invocations.first[1][:password]
|
||||
assert_equal %w(publickey hostbased),
|
||||
MockSSH.invocations.first[1][:auth_methods]
|
||||
assert_nil MockSSH.invocations.first[2]
|
||||
end
|
||||
|
||||
def test_publickey_auth_succeeds_explicit_port_no_block
|
||||
Net.const_during(:SSH, MockSSH) do
|
||||
SwitchTower::SSH.connect('demo.server.i', @config, 23)
|
||||
end
|
||||
|
||||
assert_equal 1, MockSSH.invocations.length
|
||||
assert_equal 23, MockSSH.invocations.first[1][:port]
|
||||
assert_nil MockSSH.invocations.first[2]
|
||||
end
|
||||
|
||||
def test_publickey_auth_succeeds_with_block
|
||||
Net.const_during(:SSH, MockSSH) do
|
||||
SwitchTower::SSH.connect('demo.server.i', @config) do |session|
|
||||
end
|
||||
end
|
||||
|
||||
assert_equal 1, MockSSH.invocations.length
|
||||
assert_instance_of Proc, MockSSH.invocations.first[2]
|
||||
end
|
||||
|
||||
def test_publickey_auth_fails
|
||||
MockSSH.story << Net::SSH::AuthenticationFailed
|
||||
|
||||
Net.const_during(:SSH, MockSSH) do
|
||||
SwitchTower::SSH.connect('demo.server.i', @config)
|
||||
end
|
||||
|
||||
assert_equal 2, MockSSH.invocations.length
|
||||
|
||||
assert_nil MockSSH.invocations.first[1][:password]
|
||||
assert_equal %w(publickey hostbased),
|
||||
MockSSH.invocations.first[1][:auth_methods]
|
||||
|
||||
assert_equal 'c0c0nutfr0st1ng', MockSSH.invocations.last[1][:password]
|
||||
assert_equal %w(password keyboard-interactive),
|
||||
MockSSH.invocations.last[1][:auth_methods]
|
||||
end
|
||||
|
||||
def test_password_auth_fails
|
||||
MockSSH.story << Net::SSH::AuthenticationFailed
|
||||
MockSSH.story << Net::SSH::AuthenticationFailed
|
||||
|
||||
Net.const_during(:SSH, MockSSH) do
|
||||
assert_raises(Net::SSH::AuthenticationFailed) do
|
||||
SwitchTower::SSH.connect('demo.server.i', @config)
|
||||
end
|
||||
end
|
||||
|
||||
assert_equal 2, MockSSH.invocations.length
|
||||
|
||||
assert_nil MockSSH.invocations.first[1][:password]
|
||||
assert_equal %w(publickey hostbased),
|
||||
MockSSH.invocations.first[1][:auth_methods]
|
||||
|
||||
assert_equal 'c0c0nutfr0st1ng', MockSSH.invocations.last[1][:password]
|
||||
assert_equal %w(password keyboard-interactive),
|
||||
MockSSH.invocations.last[1][:auth_methods]
|
||||
end
|
||||
end
|
36
test/utils.rb
Normal file
36
test/utils.rb
Normal file
|
@ -0,0 +1,36 @@
|
|||
class Module
|
||||
def const_during(constant, value)
|
||||
if const_defined?(constant)
|
||||
overridden = true
|
||||
saved = const_get(constant)
|
||||
remove_const(constant)
|
||||
end
|
||||
|
||||
const_set(constant, value)
|
||||
yield
|
||||
ensure
|
||||
if overridden
|
||||
remove_const(constant)
|
||||
const_set(constant, saved)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class MockLogger
|
||||
def info(msg,pfx=nil) end
|
||||
def debug(msg,pfx=nil) end
|
||||
end
|
||||
|
||||
class MockConfiguration < Hash
|
||||
def logger
|
||||
@logger ||= MockLogger.new
|
||||
end
|
||||
|
||||
def method_missing(sym, *args)
|
||||
if args.length == 0
|
||||
self[sym]
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Reference in a new issue