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*
|
*SVN*
|
||||||
|
|
||||||
|
* Works with public keys now, for passwordless deployment
|
||||||
|
|
||||||
* Subversion module recognizes the password prompt for HTTP authentication
|
* Subversion module recognizes the password prompt for HTTP authentication
|
||||||
|
|
||||||
* Preserve +x on scripts when using darcs #1929 [Scott Barron]
|
* Preserve +x on scripts when using darcs #1929 [Scott Barron]
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
require 'erb'
|
require 'erb'
|
||||||
require 'net/ssh'
|
|
||||||
require 'switchtower/command'
|
require 'switchtower/command'
|
||||||
require 'switchtower/gateway'
|
require 'switchtower/gateway'
|
||||||
|
require 'switchtower/ssh'
|
||||||
|
|
||||||
module SwitchTower
|
module SwitchTower
|
||||||
|
|
||||||
|
@ -12,7 +12,7 @@ module SwitchTower
|
||||||
# new actor via Configuration#actor.
|
# new actor via Configuration#actor.
|
||||||
class 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.
|
# Gateway class.
|
||||||
class DefaultConnectionFactory #:nodoc:
|
class DefaultConnectionFactory #:nodoc:
|
||||||
def initialize(config)
|
def initialize(config)
|
||||||
|
@ -20,8 +20,7 @@ module SwitchTower
|
||||||
end
|
end
|
||||||
|
|
||||||
def connect_to(server)
|
def connect_to(server)
|
||||||
Net::SSH.start(server, :username => @config.user,
|
SSH.connect(server, @config)
|
||||||
:password => @config.password)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -40,7 +39,7 @@ module SwitchTower
|
||||||
# instances of Actor::Task.
|
# instances of Actor::Task.
|
||||||
attr_reader :tasks
|
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
|
# Because sessions are constructed lazily, this will only contain
|
||||||
# connections to those servers that have been the targets of one or more
|
# connections to those servers that have been the targets of one or more
|
||||||
# executed tasks.
|
# executed tasks.
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
require 'thread'
|
require 'thread'
|
||||||
require 'net/ssh'
|
require 'switchtower/ssh'
|
||||||
|
|
||||||
Thread.abort_on_exception = true
|
Thread.abort_on_exception = true
|
||||||
|
|
||||||
|
@ -36,9 +36,7 @@ module SwitchTower
|
||||||
|
|
||||||
@thread = Thread.new do
|
@thread = Thread.new do
|
||||||
@config.logger.trace "starting connection to gateway #{server}"
|
@config.logger.trace "starting connection to gateway #{server}"
|
||||||
Net::SSH.start(server, :username => @config.user,
|
SSH.connect(server, @config) do |@session|
|
||||||
:password => @config.password
|
|
||||||
) do |@session|
|
|
||||||
@config.logger.trace "gateway connection established"
|
@config.logger.trace "gateway connection established"
|
||||||
@mutex.synchronize { waiter.signal }
|
@mutex.synchronize { waiter.signal }
|
||||||
connection = @session.registry[:connection][:driver]
|
connection = @session.registry[:connection][:driver]
|
||||||
|
@ -93,9 +91,8 @@ module SwitchTower
|
||||||
|
|
||||||
begin
|
begin
|
||||||
@session.forward.local(port, key, 22)
|
@session.forward.local(port, key, 22)
|
||||||
@pending_forward_requests[key] =
|
@pending_forward_requests[key] = SSH.connect('127.0.0.1', @config,
|
||||||
Net::SSH.start('127.0.0.1', :username => @config.user,
|
port)
|
||||||
:password => @config.password, :port => port)
|
|
||||||
@config.logger.trace "connection to #{key} via gateway established"
|
@config.logger.trace "connection to #{key} via gateway established"
|
||||||
rescue Object
|
rescue Object
|
||||||
@pending_forward_requests[key] = nil
|
@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"
|
$:.unshift File.dirname(__FILE__) + "/../../lib"
|
||||||
|
|
||||||
|
require File.dirname(__FILE__) + "/../utils"
|
||||||
require 'test/unit'
|
require 'test/unit'
|
||||||
require 'switchtower/scm/subversion'
|
require 'switchtower/scm/subversion'
|
||||||
|
|
||||||
class ScmSubversionTest < Test::Unit::TestCase
|
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
|
class SubversionTest < SwitchTower::SCM::Subversion
|
||||||
attr_accessor :story
|
attr_accessor :story
|
||||||
attr_reader :last_path
|
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