Permalink
Fetching contributors…
Cannot retrieve contributors at this time
285 lines (250 sloc) 8.25 KB
require 'net/ssh/gateway'
require 'net/scp'
module Sprinkle
module Actors
# 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
#
#
# == Use ssh key file
#
# deployment do
# delivery :ssh do
# user "sprinkle"
# keys "/path/to/ssh/key/file" # passed directly to Net::SSH as :keys option
#
# 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 < Actor
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)
@roles = roles
end
# Determines if there are any servers for the given roles
def servers_for_role?(roles)
roles=Array(roles)
roles.any? { |r| @roles.keys.include? (r) }
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 keys(keys)
@options[:keys] = keys
end
# Set this to true to prepend 'sudo' to every command.
def use_sudo(value=true)
@options[:use_sudo] = value
end
def sudo?
@options[:use_sudo]
end
def sudo_command
"sudo"
end
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)
rescue SSHCommandFailure => e
false
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 raise_error(e)
raise Sprinkle::Errors::RemoteCommandFailure.new(@installer, e.details, e)
end
def process(name, commands, roles, opts = {}) #:nodoc:
setup_gateway
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
end
def prepare_commands(commands)
return commands unless sudo?
commands.map do |command|
next command if command.is_a?(Symbol)
command.match(/^#{sudo_command}/) ? command : "#{sudo_command} #{command}"
end
end
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
fail=SSHCommandFailure.new
fail.details = @log_recorder.hash.merge(:hosts => host)
raise fail
end
end
true
end
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 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
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
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
end
channel.on_request("exit-signal") do |ch, data|
logger.debug red("#{cmd} was signaled!: #{data.read_long}")
end
channel.exec command do |ch, status|
logger.error("couldn't run remote command #{cmd}") unless status
@log_recorder.code = -1
end
end
session.loop
@log_recorder.code
end
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], :keys => @options[:keys])
end
end
private
def color(code, s)
"\033[%sm%s\033[0m"%[code,s]
end
def red(s)
color(31, s)
end
def yellow(s)
color(33, s)
end
def green(s)
color(32, s)
end
def blue(s)
color(34, s)
end
end
end
end