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