Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

command execution via net-ssh v2 and net-ssh-gateway working

  • Loading branch information...
commit f05ca93dcd31ac4ca1db272b1d2427f6fc5017bb 1 parent 5ac564c
@jamis jamis authored
View
8 capistrano.gemspec
@@ -1,5 +1,3 @@
-require './lib/capistrano/version'
-
Gem::Specification.new do |s|
s.name = 'capistrano'
@@ -11,14 +9,14 @@ Gem::Specification.new do |s|
DESC
s.files = Dir.glob("{bin,lib,examples,test}/**/*") + %w(README MIT-LICENSE CHANGELOG)
- s.require_path = 'lib'
s.has_rdoc = true
s.bindir = "bin"
s.executables << "cap" << "capify"
- s.add_dependency 'net-ssh', ">= #{Capistrano::Version::SSH_REQUIRED.join(".")}", "< 1.99.0"
- s.add_dependency 'net-sftp', ">= #{Capistrano::Version::SFTP_REQUIRED.join(".")}", "< 1.99.0"
+ s.add_dependency 'net-ssh', ">= 1.99.1"
+ s.add_dependency 'net-sftp', ">= 1.99.0"
+ s.add_dependency 'net-ssh-gateway', ">= 0.99.0"
s.add_dependency 'highline'
s.author = "Jamis Buck"
View
59 lib/capistrano/command.rb
@@ -36,15 +36,14 @@ def process!
loop do
active = 0
@channels.each do |ch|
- next if ch[:closed]
- active += 1
- ch.connection.process(true)
+ active += 1 unless ch[:closed]
+ ch.connection.process(0)
end
break if active == 0
if Time.now - since >= 1
since = Time.now
- @channels.each { |ch| ch.connection.ping! }
+ @channels.each { |ch| ch.connection.send_global_request("keep-alive@openssh.com") }
end
sleep 0.01 # a brief respite, to keep the CPU from going crazy
end
@@ -84,38 +83,32 @@ def open_channels
channel[:host] = server.host
channel[:options] = options
- execute_command = Proc.new do |ch|
- logger.trace "executing command", ch[:server] if logger
- cmd = replace_placeholders(command, ch)
+ request_pty_if_necessary(channel) do |ch, success|
+ if success
+ logger.trace "executing command", ch[:server] if logger
+ cmd = replace_placeholders(command, ch)
- if options[:shell] == false
- shell = nil
- else
- shell = "#{options[:shell] || "sh"} -c"
- cmd = cmd.gsub(/[$\\`"]/) { |m| "\\#{m}" }
- cmd = "\"#{cmd}\""
- end
+ if options[:shell] == false
+ shell = nil
+ else
+ shell = "#{options[:shell] || "sh"} -c"
+ cmd = cmd.gsub(/[$\\`"]/) { |m| "\\#{m}" }
+ cmd = "\"#{cmd}\""
+ end
- command_line = [environment, shell, cmd].compact.join(" ")
+ command_line = [environment, shell, cmd].compact.join(" ")
- ch.exec(command_line)
- ch.send_data(options[:data]) if options[:data]
- end
-
- if options[:pty]
- channel.request_pty(:want_reply => true)
- channel.on_success(&execute_command)
- channel.on_failure do |ch|
+ ch.exec(command_line)
+ ch.send_data(options[:data]) if options[:data]
+ else
# 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[:server] if logger
ch.close
end
- else
- execute_command.call(channel)
end
-
+
channel.on_data do |ch, data|
@callback[ch, :out, data] if @callback
end
@@ -124,8 +117,8 @@ def open_channels
@callback[ch, :err, data] if @callback
end
- channel.on_request do |ch, request, reply, data|
- ch[:status] = data.read_long if request == "exit-status"
+ channel.on_request("exit-status") do |ch, data|
+ ch[:status] = data.read_long
end
channel.on_close do |ch|
@@ -135,6 +128,16 @@ def open_channels
end
end
+ def request_pty_if_necessary(channel)
+ if options[:pty]
+ channel.request_pty do |ch, success|
+ yield ch, success
+ end
+ else
+ yield channel, true
+ end
+ end
+
def replace_placeholders(command, channel)
command.gsub(/\$CAPISTRANO:HOST\$/, channel[:host])
end
View
3  lib/capistrano/configuration/actions/file_transfer.rb
@@ -23,8 +23,7 @@ def get(remote_path, path, options = {})
execute_on_servers(options.merge(:once => true)) do |servers|
logger.info "downloading `#{servers.first.host}:#{remote_path}' to `#{path}'"
sftp = sessions[servers.first].sftp
- sftp.connect unless sftp.state == :open
- sftp.get_file remote_path, path
+ sftp.download! remote_path, path
logger.debug "download finished"
end
end
View
42 lib/capistrano/configuration/connections.rb
@@ -1,6 +1,6 @@
require 'enumerator'
-require 'capistrano/gateway'
+require 'net/ssh/gateway'
require 'capistrano/ssh'
module Capistrano
@@ -11,8 +11,6 @@ def self.included(base) #:nodoc:
base.send :alias_method, :initialize, :initialize_with_connections
end
- # An adaptor for making the SSH interface look and act like that of the
- # Gateway class.
class DefaultConnectionFactory #:nodoc:
def initialize(options)
@options = options
@@ -23,6 +21,19 @@ def connect_to(server)
end
end
+ class GatewayConnectionFactory #:nodoc:
+ def initialize(gateway, options)
+ server = ServerDefinition.new(gateway)
+ @gateway = Net::SSH::Gateway.new(server.host, server.user || ServerDefinition.default_user, server.options)
+ @options = options
+ end
+
+ def connect_to(server)
+ local_host = ServerDefinition.new("127.0.0.1", :user => server.user, :port => @gateway.open(server.host, server.port || 22))
+ SSH.connect(local_host, @options)
+ end
+ end
+
# 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
@@ -61,7 +72,7 @@ def connection_factory
@connection_factory ||= begin
if exists?(:gateway)
logger.debug "establishing connection to gateway `#{fetch(:gateway)}'"
- Gateway.new(ServerDefinition.new(fetch(:gateway)), self)
+ GatewayConnectionFactory.new(fetch(:gateway), self)
else
DefaultConnectionFactory.new(self)
end
@@ -72,22 +83,13 @@ def connection_factory
def establish_connections_to(servers)
failed_servers = []
- # This attemps to work around the problem where SFTP uploads hang
- # for some people. A bit of investigating seemed to reveal that the
- # hang only occurred when the SSH connections were established async,
- # so this setting allows people to at least work around the problem.
- if fetch(:synchronous_connect, false)
- logger.trace "synchronous_connect: true"
- Array(servers).each { |server| safely_establish_connection_to(server, failed_servers) }
- else
- # force the connection factory to be instantiated synchronously,
- # otherwise we wind up with multiple gateway instances, because
- # each connection is done in parallel.
- connection_factory
+ # force the connection factory to be instantiated synchronously,
+ # otherwise we wind up with multiple gateway instances, because
+ # each connection is done in parallel.
+ connection_factory
- threads = Array(servers).map { |server| establish_connection_to(server, failed_servers) }
- threads.each { |t| t.join }
- end
+ threads = Array(servers).map { |server| establish_connection_to(server, failed_servers) }
+ threads.each { |t| t.join }
if failed_servers.any?
errors = failed_servers.map { |h| "#{h[:server]} (#{h[:error].class}: #{h[:error].message})" }
@@ -171,6 +173,8 @@ def establish_connection_to(server, failures=nil)
def safely_establish_connection_to(server, failures=nil)
sessions[server] ||= connection_factory.connect_to(server)
rescue Exception => err
+puts err
+puts err.backtrace
raise unless failures
failures << { :server => server, :error => err }
end
View
131 lib/capistrano/gateway.rb
@@ -1,131 +0,0 @@
-if RUBY_VERSION == "1.8.6"
- begin
- require 'fastthread'
- rescue LoadError
- warn "You are running Ruby 1.8.6, which has a bug in its threading implementation."
- warn "You are liable to encounter deadlocks running Capistrano, unless you install"
- warn "the fastthread library, which is available as a gem:"
- warn " gem install fastthread"
- end
-end
-
-require 'thread'
-require 'capistrano/errors'
-require 'capistrano/ssh'
-require 'capistrano/server_definition'
-
-Thread.abort_on_exception = true
-
-module Capistrano
-
- # Black magic. It uses threads and Net::SSH to set up a connection to a
- # gateway server, through which connections to other servers may be
- # tunnelled.
- #
- # It is used internally by Capistrano, but may be useful on its own, as well.
- #
- # Usage:
- #
- # gateway = Capistrano::Gateway.new(Capistrano::ServerDefinition.new('gateway.example.com'))
- #
- # sess1 = gateway.connect_to(Capistrano::ServerDefinition.new('hidden.example.com'))
- # sess2 = gateway.connect_to(Capistrano::ServerDefinition.new('other.example.com'))
- class Gateway
- # The Thread instance driving the gateway connection.
- attr_reader :thread
-
- # The Net::SSH session representing the gateway connection.
- attr_reader :session
-
- MAX_PORT = 65535
- MIN_PORT = 1024
-
- def initialize(server, options={}) #:nodoc:
- @options = options
- @next_port = MAX_PORT
- @terminate_thread = false
- @port_guard = Mutex.new
-
- mutex = Mutex.new
- waiter = ConditionVariable.new
-
- mutex.synchronize do
- @thread = Thread.new do
- 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 }
- @session.loop do
- !@terminate_thread
- end
- end
- end
-
- waiter.wait(mutex)
- end
- end
-
- # Shuts down all forwarded connections and terminates the gateway.
- def shutdown!
- # cancel all active forward channels
- session.forward.active_locals.each do |lport, host, port|
- session.forward.cancel_local(lport)
- end
-
- # terminate the gateway thread
- @terminate_thread = true
-
- # wait for the gateway thread to stop
- thread.join
- end
-
- # Connects to the given server by opening a forwarded port from the local
- # host to the server, via the gateway, and then opens and returns a new
- # Net::SSH connection via that port.
- def connect_to(server)
- connection = nil
- 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.xserver = server
- logger.trace "connected: `#{server}' (via gateway)" if logger
- rescue Errno::EADDRINUSE
- local_port = next_port
- retry
- rescue Exception => e
- warn "#{e.class}: #{e.message}"
- warn e.backtrace.join("\n")
- end
- end
-
- thread.join
- if connection.nil?
- error = ConnectionError.new("could not establish connection to `#{server}'")
- error.hosts = [server]
- raise error
- end
-
- connection
- end
-
- private
-
- def logger
- @options[:logger]
- end
-
- def next_port
- @port_guard.synchronize do
- port = @next_port
- @next_port -= 1
- @next_port = MAX_PORT if @next_port < MIN_PORT
- port
- end
- end
- end
-end
View
5 lib/capistrano/server_definition.rb
@@ -7,6 +7,11 @@ class ServerDefinition
attr_reader :port
attr_reader :options
+ # The default user name to use when a user name is not explicitly provided
+ def self.default_user
+ ENV['USER'] || ENV['USERNAME'] || "not-specified"
+ end
+
def initialize(string, options={})
@user, @host, @port = string.match(/^(?:([^;,:=]+)@|)(.*?)(?::(\d+)|)$/)[1,3]
View
57 lib/capistrano/ssh.rb
@@ -1,61 +1,12 @@
begin
require 'rubygems'
- gem 'net-ssh', "< 1.99.0"
+ gem 'net-ssh', ">= 1.99.1"
rescue LoadError, NameError
end
require 'net/ssh'
module Capistrano
- unless ENV['SKIP_VERSION_CHECK']
- require 'capistrano/version'
- require 'net/ssh/version'
- ssh_version = [Net::SSH::Version::MAJOR, Net::SSH::Version::MINOR, Net::SSH::Version::TINY]
- if !Version.check(Version::SSH_REQUIRED, ssh_version)
- raise "You have Net::SSH #{ssh_version.join(".")}, but you need at least #{Version::SSH_REQUIRED.join(".")}"
- end
- end
-
- # Now, Net::SSH is kind of silly, and tries to lazy-load everything. This
- # wreaks havoc with the parallel connection trick that Capistrano wants to
- # use, so we're going to do something hideously ugly here and force all the
- # files that Net::SSH uses to load RIGHT NOW, rather than lazily.
-
- net_ssh_dependencies = %w(connection/services connection/channel connection/driver
- service/agentforward/services service/agentforward/driver
- service/process/driver util/prompter
- service/forward/services service/forward/driver service/forward/local-network-handler service/forward/remote-network-handler
- service/shell/services service/shell/driver
- lenient-host-key-verifier
- transport/compress/services transport/compress/zlib-compressor transport/compress/none-compressor transport/compress/zlib-decompressor transport/compress/none-decompressor
- transport/kex/services transport/kex/dh transport/kex/dh-gex
- transport/ossl/services
- transport/ossl/hmac/services transport/ossl/hmac/sha1 transport/ossl/hmac/sha1-96 transport/ossl/hmac/md5 transport/ossl/hmac/md5-96 transport/ossl/hmac/none
- transport/ossl/cipher-factory transport/ossl/hmac-factory transport/ossl/buffer-factory transport/ossl/key-factory transport/ossl/digest-factory
- transport/identity-cipher transport/packet-stream transport/version-negotiator transport/algorithm-negotiator transport/session
- userauth/methods/services userauth/methods/password userauth/methods/keyboard-interactive userauth/methods/publickey userauth/methods/hostbased
- userauth/services userauth/agent userauth/userkeys userauth/driver
- transport/services service/services
- )
-
- net_ssh_dependencies << "userauth/pageant" if File::ALT_SEPARATOR
- net_ssh_dependencies.each do |path|
- begin
- require "net/ssh/#{path}"
- rescue LoadError
- # Ignore load errors from this, since some files are in the list which
- # do not exist in different (supported) versions of Net::SSH. We know
- # (by this point) that Net::SSH is installed, though, since we do a
- # require 'net/ssh' at the very top of this file, and we know the
- # installed version meets the minimum version requirements because of
- # the version check, also at the top of this file. So, if we get a
- # LoadError, it's simply because the file in question does not exist in
- # the version of Net::SSH that is installed.
- #
- # Whew!
- end
- end
-
# A helper class for dealing with SSH connections.
class SSH
# Patch an accessor onto an SSH connection so that we can record the server
@@ -93,8 +44,8 @@ def self.connect(server, options={}, &block)
password_value = nil
ssh_options = (server.options[:ssh_options] || {}).dup.merge(options[:ssh_options] || {}).dup
- ssh_options[:username] = server.user || options[:user] || ssh_options[:username]
- ssh_options[:port] = server.port || options[:port] || ssh_options[:port] || DEFAULT_PORT
+ user = server.user || options[:user] || ssh_options[:username] || ServerDefinition.default_user
+ ssh_options[:port] = server.port || options[:port] || ssh_options[:port] || DEFAULT_PORT
begin
connection_options = ssh_options.merge(
@@ -102,7 +53,7 @@ def self.connect(server, options={}, &block)
:auth_methods => ssh_options[:auth_methods] || methods.shift
)
- connection = Net::SSH.start(server.host, connection_options, &block)
+ connection = Net::SSH.start(server.host, user, connection_options, &block)
Server.apply_to(connection, server)
rescue Net::SSH::AuthenticationFailed
View
80 lib/capistrano/upload.rb
@@ -1,24 +1,13 @@
begin
require 'rubygems'
- gem 'net-sftp', "< 1.99.0"
+# gem 'net-sftp', ">= 1.99.0"
rescue LoadError, NameError
end
require 'net/sftp'
-require 'net/sftp/operations/errors'
require 'capistrano/errors'
module Capistrano
- unless ENV['SKIP_VERSION_CHECK']
- require 'capistrano/version'
- require 'net/sftp/version'
- sftp_version = [Net::SFTP::Version::MAJOR, Net::SFTP::Version::MINOR, Net::SFTP::Version::TINY]
- required_version = [1,1,0]
- if !Capistrano::Version.check(required_version, sftp_version)
- raise "You have Net::SFTP #{sftp_version.join(".")}, but you need at least #{required_version.join(".")}. Net::SFTP will not be used."
- end
- end
-
# This class encapsulates a single file upload to be performed in parallel
# across multiple machines, using the SFTP protocol. Although it is intended
# to be used primarily from within Capistrano, it may also be used standalone
@@ -59,7 +48,7 @@ def initialize(sessions, filename, options)
@options = options
@completed = @failed = 0
- @sftps = setup_sftp
+ @uploaders = setup_uploaders
end
# Uploads to all specified servers in parallel. If any one of the servers
@@ -67,21 +56,20 @@ def initialize(sessions, filename, options)
def process!
logger.debug "uploading #{filename}" if logger
while running?
- @sftps.each do |sftp|
- next if sftp.channel[:done]
+ sessions.each do |session|
begin
- sftp.channel.connection.process(true)
- rescue Net::SFTP::Operations::StatusException => error
- logger.important "uploading failed: #{error.description}", sftp.channel[:server] if logger
- failed!(sftp)
+ session.process(0)
+ rescue Net::SFTP::StatusException => error
+ logger.important "uploading failed: #{error.description}", session.xserver if logger
+ failed!(uploader)
end
end
sleep 0.01 # a brief respite, to keep the CPU from going crazy
end
logger.trace "upload finished" if logger
- if (failed = @sftps.select { |sftp| sftp.channel[:failed] }).any?
- hosts = failed.map { |sftp| sftp.channel[:server] }
+ if (failed = @uploaders.select { |uploader| uploader[:failed] }).any?
+ hosts = failed.map { |uploader| uploader[:server] }
error = UploadError.new("upload of #{filename} failed on #{hosts.join(',')}")
error.hosts = hosts
raise error
@@ -96,56 +84,36 @@ def logger
options[:logger]
end
- def setup_sftp
+ def setup_uploaders
sessions.map do |session|
server = session.xserver
sftp = session.sftp
- sftp.connect unless sftp.state == :open
-
- sftp.channel[:server] = server
- sftp.channel[:done] = false
- sftp.channel[:failed] = false
real_filename = filename.gsub(/\$CAPISTRANO:HOST\$/, server.host)
- sftp.open(real_filename, IO::WRONLY | IO::CREAT | IO::TRUNC, options[:mode] || 0664) do |status, handle|
- break unless check_status(sftp, "open #{real_filename}", server, status)
-
- logger.info "uploading data to #{server}:#{real_filename}" if logger
- sftp.write(handle, options[:data] || "") do |status|
- break unless check_status(sftp, "write to #{server}:#{real_filename}", server, status)
- sftp.close_handle(handle) do
- logger.debug "done uploading data to #{server}:#{real_filename}" if logger
- completed!(sftp)
- end
- end
- end
-
- sftp
- end
- end
-
- def check_status(sftp, action, server, status)
- return true if status.code == Net::SFTP::Session::FX_OK
+ logger.info "uploading data to #{server}:#{real_filename}" if logger
+ uploader = sftp.upload(StringIO.new(options[:data] || ""), real_filename, :permissions => options[:mode] || 0664)
- logger.error "could not #{action} on #{server} (#{status.message})" if logger
- failed!(sftp)
+ uploader[:server] = server
+ uploader[:done] = false
+ uploader[:failed] = false
- return false
+ uploader
+ end
end
-
+
def running?
- completed < @sftps.length
+ completed < @uploaders.length
end
- def failed!(sftp)
- completed!(sftp)
+ def failed!(uploader)
+ completed!(uploader)
@failed += 1
- sftp.channel[:failed] = true
+ uploader[:failed] = true
end
- def completed!(sftp)
+ def completed!(uploader)
@completed += 1
- sftp.channel[:done] = true
+ uploader[:done] = true
end
end

0 comments on commit f05ca93

Please sign in to comment.
Something went wrong with that request. Please try again.