lib/sprinkle/actors/ssh.rb in sprinkle-0.4.2 vs lib/sprinkle/actors/ssh.rb in sprinkle-0.5.0.rc1
- old
+ new
@@ -1,152 +1,245 @@
require 'net/ssh/gateway'
require 'net/scp'
module Sprinkle
module Actors
- class Ssh
- attr_accessor :options
-
+ # The SSH actor requires no additional deployment tools other than the
+ # Ruby SSH libraries.
+ #
+ # deployment do
+ # delivery :ssh do
+ # user "rails"
+ # password "leetz"
+ #
+ # role :app, "app.myserver.com"
+ # end
+ # end
+ #
+ #
+ # == Working thru a gateway
+ #
+ # If you're behind a firewall and need to use a SSH gateway that's fine.
+ #
+ # deployment do
+ # delivery :ssh do
+ # gateway "work.sshgateway.com"
+ # end
+ # end
+ class SSH
+ attr_accessor :options #:nodoc:
+
+ class SSHCommandFailure < StandardError #:nodoc:
+ attr_accessor :details
+ end
+
+ class SSHConnectionCache
+ def initialize; @cache={}; end
+ def start(host, user, opts={})
+ key="#{host}#{user}#{opts.to_s}"
+ @cache[key] ||= Net::SSH.start(host,user,opts)
+ end
+ end
+
+
def initialize(options = {}, &block) #:nodoc:
@options = options.update(:user => 'root')
+ @roles = {}
+ @connection_cache = SSHConnectionCache.new
self.instance_eval &block if block
+ raise "You must define at least a single role." if @roles.empty?
end
+ # Define a whole host of roles at once
+ #
+ # This is depreciated - you should be using role instead.
def roles(roles)
- @options[:roles] = roles
+ @roles = roles
end
+
+ # Define a role and add servers to it
+ #
+ # role :app, "app.server.com"
+ # role :db, "db.server.com"
+ def role(role, server)
+ @roles[role] ||= []
+ @roles[role] << server
+ end
+ # Set an optional SSH gateway server - if set all outbound SSH traffic
+ # will go thru this gateway
def gateway(gateway)
@options[:gateway] = gateway
end
+ # Set the SSH user
def user(user)
@options[:user] = user
end
+ # Set the SSH password
def password(password)
@options[:password] = password
end
- def process(name, commands, roles, suppress_and_return_failures = false)
- return process_with_gateway(name, commands, roles) if gateway_defined?
- r = process_direct(name, commands, roles)
- logger.debug green "process returning #{r}"
- return r
+ # Set this to true to prepend 'sudo' to every command.
+ def use_sudo(value)
+ @options[:use_sudo] = value
end
- def transfer(name, source, destination, roles, recursive = true, suppress_and_return_failures = false)
- return transfer_with_gateway(name, source, destination, roles, recursive) if gateway_defined?
- transfer_direct(name, source, destination, roles, recursive)
+ def setup_gateway #:nodoc:
+ @gateway ||= Net::SSH::Gateway.new(@options[:gateway], @options[:user]) if @options[:gateway]
end
-
+
+ def teardown #:nodoc:
+ @gateway.shutdown! if @gateway
+ end
+
+ def verify(verifier, roles, opts = {}) #:nodoc:
+ @verifier = verifier
+ # issue all the verification steps in a single SSH command
+ commands=[verifier.commands.join(" && ")]
+ process(verifier.package.name, commands, roles,
+ :suppress_and_return_failures => true)
+ ensure
+ @verifier = nil
+ end
+
+ def install(installer, roles, opts = {}) #:nodoc:
+ @installer = installer
+ process(installer.package.name, installer.install_sequence, roles)
+ rescue SSHCommandFailure => e
+ raise_error(e)
+ ensure
+ @installer = nil
+ end
+
protected
- def process_with_gateway(name, commands, roles)
- res = []
- on_gateway do |gateway|
- Array(roles).each { |role| res << execute_on_role(commands, role, gateway) }
+ def raise_error(e)
+ raise Sprinkle::Errors::RemoteCommandFailure.new(@installer, e.details, e)
+ end
+
+ def process(name, commands, roles, opts = {}) #:nodoc:
+ opts.reverse_merge!(:suppress_and_return_failures => false)
+ setup_gateway
+ @suppress = opts[:suppress_and_return_failures]
+ r=execute_on_role(commands, roles)
+ logger.debug green "process returning #{r}"
+ return r
+ end
+
+ def execute_on_role(commands, role) #:nodoc:
+ hosts = @roles[role]
+ Array(hosts).each do |host|
+ success = execute_on_host(commands, host)
+ return false unless success
end
- !(res.include? false)
end
- def process_direct(name, commands, roles)
- res = []
- Array(roles).each { |role| res << execute_on_role(commands, role) }
- !(res.include? false)
+ def prepare_commands(commands)
+ return commands unless @options[:use_sudo]
+ commands.map do |command|
+ next command if command.is_a?(Symbol)
+ command.match(/^sudo/) ? command : "sudo #{command}"
+ end
end
- def transfer_with_gateway(name, source, destination, roles, recursive)
- on_gateway do |gateway|
- Array(roles).each { |role| transfer_to_role(source, destination, role, recursive, gateway) }
+ def execute_on_host(commands,host) #:nodoc:
+ session = ssh_session(host)
+ @log_recorder = Sprinkle::Utility::LogRecorder.new
+ prepare_commands(commands).each do |cmd|
+ if cmd == :TRANSFER
+ transfer_to_host(@installer.sourcepath, @installer.destination, session,
+ :recursive => @installer.options[:recursive])
+ next
+ elsif cmd == :RECONNECT
+ session.close # disconnenct
+ session = ssh_session(host) # reconnect
+ next
+ end
+ @log_recorder.reset cmd
+ res = ssh(session, cmd)
+ if res != 0
+ if @suppress
+ return false
+ else
+ fail=SSHCommandFailure.new
+ fail.details = @log_recorder.hash.merge(:hosts => host)
+ raise fail, "#{cmd} failed with error code #{res[:code]}"
+ end
+ end
end
+ true
end
- def transfer_direct(name, source, destination, roles, recursive)
- Array(roles).each { |role| transfer_to_role(source, destination, role, recursive) }
+ def ssh(host, cmd, opts={}) #:nodoc:
+ logger.debug "ssh: #{cmd}"
+ session = host.is_a?(Net::SSH::Connection::Session) ? host : ssh_session(host)
+ channel_runner(session, cmd)
end
-
- def execute_on_role(commands, role, gateway = nil)
- hosts = @options[:roles][role]
- res = []
- Array(hosts).each { |host| res << execute_on_host(commands, host, gateway) }
- !(res.include? false)
- end
-
- def transfer_to_role(source, destination, role, gateway = nil)
- hosts = @options[:roles][role]
- Array(hosts).each { |host| transfer_to_host(source, destination, host, gateway) }
- end
- def execute_on_host(commands, host, gateway = nil)
- res = nil
- logger.debug(blue "executing #{commands.inspect} on #{host}.")
- if gateway # SSH connection via gateway
- gateway.ssh(host, @options[:user]) do |ssh|
- res = execute_on_connection(commands, ssh)
- ssh.loop
+ def channel_runner(session, command) #:nodoc:
+ session.open_channel do |channel|
+ channel.on_data do |ch, data|
+ @log_recorder.log :out, data
+ logger.debug yellow("stdout said-->\n#{data}\n")
end
- else # direct SSH connection
- Net::SSH.start(host, @options[:user], :password => @options[:password]) do |ssh|
- res = execute_on_connection(commands, ssh)
- ssh.loop
+ channel.on_extended_data do |ch, type, data|
+ next unless type == 1 # only handle stderr
+ @log_recorder.log :err, data
+ logger.debug red("stderr said -->\n#{data}\n")
end
- end
- res.detect{|x| x!=0}.nil?
- end
- def execute_on_connection(commands, session)
- res = []
- Array(commands).each do |cmd|
- session.open_channel do |channel|
- channel.on_data do |ch, data|
- logger.debug yellow("stdout said-->\n#{data}\n")
+ channel.on_request("exit-status") do |ch, data|
+ @log_recorder.code = data.read_long
+ if @log_recorder.code == 0
+ logger.debug(green 'success')
+ else
+ logger.debug(red('failed (%d).' % @log_recorder.code))
end
- channel.on_extended_data do |ch, type, data|
- next unless type == 1 # only handle stderr
- logger.debug red("stderr said -->\n#{data}\n")
- end
+ end
- channel.on_request("exit-status") do |ch, data|
- exit_code = data.read_long
- if exit_code == 0
- logger.debug(green 'success')
- else
- logger.debug(red('failed (%d).'%exit_code))
- end
- res << exit_code
- end
-
- channel.on_request("exit-signal") do |ch, data|
- logger.debug red("#{cmd} was signaled!: #{data.read_long}")
- end
-
- channel.exec cmd do |ch, status|
- logger.error("couldn't run remote command #{cmd}") unless status
- end
+ channel.on_request("exit-signal") do |ch, data|
+ logger.debug red("#{cmd} was signaled!: #{data.read_long}")
end
- end
- res
- end
- def transfer_to_host(source, destination, host, recursive, gateway = nil)
- if gateway # SSH connection via gateway
- gateway.ssh(host, @options[:user]) do |ssh|
- transfer_on_connection(source, destination, recursive, ssh)
+ channel.exec command do |ch, status|
+ logger.error("couldn't run remote command #{cmd}") unless status
+ @log_recorder.code = -1
end
- else # direct SSH connection
- Net::SSH.start(host, @options[:user]) do |ssh|
- transfer_on_connection(source, destination, recursive, ssh)
- end
end
+ session.loop
+ @log_recorder.code
end
-
- def transfer_on_connection(source, destination, recursive, connection)
- scp = Net::SCP.new(connection)
- scp.upload! source, destination, :recursive => recursive
+
+ def transfer_to_role(source, destination, role, opts={}) #:nodoc:
+ hosts = @roles[role]
+ Array(hosts).each { |host| transfer_to_host(source, destination, host, opts) }
end
-
+
+ def transfer_to_host(source, destination, host, opts={}) #:nodoc:
+ logger.debug "upload: #{destination}"
+ session = host.is_a?(Net::SSH::Connection::Session) ? host : ssh_session(host)
+ scp = Net::SCP.new(session)
+ scp.upload! source, destination, :recursive => opts[:recursive], :chunk_size => 32.kilobytes
+ rescue RuntimeError => e
+ if e.message =~ /Permission denied/
+ raise TransferFailure.no_permission(@installer,e)
+ else
+ raise e
+ end
+ end
+
+ def ssh_session(host)
+ if @gateway
+ gateway.ssh(host, @options[:user])
+ else
+ @connection_cache.start(host, @options[:user],:password => @options[:password])
+ end
+ end
+
private
def color(code, s)
"\033[%sm%s\033[0m"%[code,s]
end
def red(s)
@@ -158,20 +251,9 @@
def green(s)
color(32, s)
end
def blue(s)
color(34, s)
- end
-
- def gateway_defined?
- !! @options[:gateway]
- end
-
- def on_gateway(&block)
- gateway = Net::SSH::Gateway.new(@options[:gateway], @options[:user])
- block.call gateway
- ensure
- gateway.shutdown!
end
end
end
end