lib/drbman/host_machine.rb in royw-drbman-0.0.1 vs lib/drbman/host_machine.rb in royw-drbman-0.0.2
- old
+ new
@@ -9,19 +9,28 @@
# the connection until a disconnect() is invoked.
class HostMachine
attr_accessor :uuid, :dir, :controller
attr_reader :name, :machine, :user, :port
+ class << self
+ attr_accessor :connection_mutex
+ end
+ @connection_mutex = Mutex.new
+
# @param [String] host_string describes the host to connect to.
# The format is "{user{:password}@}machine{:port}"
# @param [Logger] logger the logger to use
- def initialize(host_string, logger)
+ # @param [UserChoices,Hash] choices
+ # @option choices [Array<String>] :keys (['~/.ssh/id_dsa', '~/.ssh/id_rsa']) array of ssh key file names.
+ def initialize(host_string, logger, choices)
@logger = logger
+ @choices = choices
@machine = 'localhost'
@user = ENV['USER']
@port = 9000
- @password = {:keys => ['~/.ssh/id_dsa']}
+ keys = choices[:keys] || ["~/.ssh/id_dsa", "~/.ssh/id_rsa"]
+ @password = {:keys => keys.collect{|name| name.gsub('~', ENV['HOME'])}.select{|name| File.exist?(name)}}
case host_string
when /^(\S+)\:(\S+)\@(\S+)\:(\d+)$/
@user = $1
@password = {:password => $2}
@machine = $3
@@ -55,22 +64,32 @@
# host_machine.session do |host|
# host.upload(local_dir, "#{host.dir}/#{File.basename(local_dir)}")
# @logger.debug { host.sh("ls -lR #{host.dir}") }
# end
def session(&block)
- connect
- yield self
- disconnect
+ begin
+ # this is ugly but apparently net-ssh can fail public_key authentication
+ # when ran in parallel.
+ HostMachine.connection_mutex.synchronize do
+ connect
+ end
+ yield self
+ rescue Exception => e
+ @logger.error { e }
+ @logger.error { e.backtrace.join("\n") }
+ raise e
+ ensure
+ disconnect
+ end
end
# upload a directory structure to the host machine.
# @param [String] local_src the source directory on the local machine
# @param [String] remote_dest the destination directory on the host machine
# @raise [Exception] if the files are not copied
def upload(local_src, remote_dest)
@logger.debug { "upload(\"#{local_src}\", \"#{remote_dest}\")" }
- connect
result = nil
unless @ssh.nil?
begin
@ssh.scp.upload!(local_src, remote_dest, :recursive => true) do |ch, name, sent, total|
@logger.debug { "#{name}: #{sent}/#{total}" }
@@ -80,17 +99,16 @@
# only raise the exception if the files differ
raise e unless same_files?(local_src, remote_dest)
end
end
end
-
+
# download a directory structure from the host machine.
# @param [String] remote_src the source directory on the host machine
# @param [String] local_dest the destination directory on the local machine
# @raise [Exception] if the files are not copied
def download(remote_src, local_dest)
- connect
result = nil
unless @ssh.nil?
begin
@ssh.scp.download!(local_src, remote_dest, :recursive => true) do |ch, name, sent, total|
@logger.debug { "#{name}: #{sent}/#{total}" }
@@ -107,107 +125,84 @@
# Note that the environment on the host machine is the default environment instead
# of the user's environment. So by default we try to source ~/.profile and ~/.bashrc
#
# @param [String] command the command to run
# @param [Hash] opts
- # @options opts [Array<String>] :source array of files to source. defaults to ['~/.profile', '~/.bashrc']
+ # @option opts [Array<String>] :source (['~/.profile', '~/.bashrc']) array of files to source.
+ # @return [String, nil] the output from running the command
def sh(command, opts={})
- @logger.debug { "sh \"#{command}\""}
- # if opts[:source].blank?
- # opts[:source] = ['~/.profile', '~/.bashrc']
- # end
- connect
- result = nil
unless @ssh.nil?
- if @pre_commands.nil?
- @pre_commands = []
- opts[:source] ||= []
- opts[:source].each do |name|
- ls_out = @ssh.exec!("ls #{name}")
- @pre_commands << "source #{name}" if ls_out =~ /^\s*\S+\/#{File.basename(name)}\s*$/
- end
- end
- commands = @pre_commands.clone
+ opts[:source] = ['~/.profile', '~/.bashrc'] if opts[:source].blank?
+ result = nil
+ commands = pre_commands(opts[:source])
commands << command
command_line = commands.join(' && ')
+ @logger.debug { "sh: \"#{command_line}\""}
result = @ssh.exec!(command_line)
+ @logger.debug { "=> #{result}" }
end
result
end
- # run a command as the superuser on the host machine
- # Note that the environment on the host machine is the default environment instead
- # of the user's environment. So by default we try to source ~/.profile and ~/.bashrc
- #
- # @param [String] command the command to run
- # @param [Hash] opts
- # @options opts [Array<String>] :source array of files to source. defaults to ['~/.profile', '~/.bashrc']
- def sudo(command, opts={})
- @logger.debug { "sudo \"#{command}\""}
- # if opts[:source].blank?
- # opts[:source] = ['~/.profile', '~/.bashrc']
- # end
- connect
- result = nil
- unless @ssh.nil?
- buf = []
- @ssh.open_channel do |channel|
- if @pre_commands.nil?
- @pre_commands = []
- opts[:source] ||= []
- opts[:source].each do |name|
- ls_out = @ssh.exec!("ls #{name}")
- @pre_commands << "source #{name}" if ls_out =~ /^\s*\S+\/#{File.basename(name)}\s*$/
- end
+ private
+
+ def pre_commands(sources)
+ if @pre_commands.nil?
+ @pre_commands = []
+ unless @ssh.nil?
+ sources.each do |name|
+ ls_out = @ssh.exec!("ls #{name}")
+ @pre_commands << "source #{name}" if ls_out =~ /^\s*\S+\/#{File.basename(name)}\s*$/
end
- commands = @pre_commands.clone
- commands << "sudo -p 'sudo password: ' #{command}"
- command_line = commands.join(' && ')
- channel.exec(command_line) do |ch, success|
- ch.on_data do |ch, data|
- if data =~ /sudo password: /
- ch.send_data("#{@password[:password]}\n")
- else
- buf << data
- end
- end
- end
end
- @ssh.loop
- result = buf.compact.join('')
end
- result
+ @pre_commands.clone
end
-
+
# connect to the host machine
+ # note, you should not need to call the connect method.
+ # @see {#session}
def connect
if @ssh.nil?
- @ssh = Net::SSH.start(@machine, @user, @password)
+ options = @password.merge({
+ :timeout=>2,
+ :auth_methods => %w(publickey hostbased password)
+ })
+ options = @password.merge({:verbose=>Logger::DEBUG}) if @choices[:ssh_debug]
+ @logger.debug { "connect: @machine=>#{@machine}, @user=>#{@user}, options=>#{options.inspect}" }
+ @ssh = Net::SSH.start(@machine, @user, options)
# @ssh.forward.local(@port, @machine, @port)
end
end
- # disconnect from the host machine
+ # disconnect from the host machine.
+ # note, you should not need to call the disconnect method.
+ # @see {#session}
def disconnect
if @ssh
@ssh.close
@ssh = nil
end
end
- private
-
# Does the local directory tree and the remote directory tree contain the same files?
# Calculates a MD5 hash for each file then compares the hashes
# @param [String] local_path local directory
# @param [String] remote_path remote directory
+ # @return [Boolean] asserted if the files in both directory trees are identical
def same_files?(local_path, remote_path)
- remote_md5 = @ssh.exec!(md5_command_line(remote_path))
- local_md5 = `#{md5_command_line(local_path)}`
- @logger.debug { "same_files? local_md5 => #{local_md5}, remote_md5 => #{remote_md5}"}
- remote_md5 == local_md5
+ result = false
+ unless @ssh.nil?
+ remote_md5 = @ssh.exec!(md5_command_line(remote_path))
+ local_md5 = `#{md5_command_line(local_path)}`
+ @logger.debug { "same_files? local_md5 => #{local_md5}, remote_md5 => #{remote_md5}"}
+ result = (remote_md5 == local_md5)
+ end
+ result
end
+ # @param [String] dirname the directory name to use in building the md5 command line
+ # @return [String] the command line for finding the md5 hash value
def md5_command_line(dirname)
line = "cat \`find #{dirname} -type f | sort\` | ruby -e \"require 'digest/md5';puts Digest::MD5.hexdigest(STDIN.read)\""
@logger.debug { line }
line
end