lib/rye/box.rb in rye-0.7.2 vs lib/rye/box.rb in rye-0.7.3

- old
+ new

@@ -34,12 +34,14 @@ def safe; @rye_safe; end def user; (@rye_opts || {})[:user]; end def host=(val); @rye_host = val; end def opts=(val); @rye_opts = val; end - def safe=(val); @rye_safe = val; end - + + def enable_safe_mode; @rye_safe = true; end + def disable_safe_mode; @rye_safe = false; end + # The most recent value from Box.cd or Box.[] def current_working_directory; @rye_current_working_directory; end # The most recent valud for umask (or 0022) def current_umask; @rye_current_umask; end @@ -98,11 +100,11 @@ @rye_debug = STDERR if @rye_debug == true @rye_error = STDERR if @rye_error == true @rye_info = STDOUT if @rye_info == true @rye_opts[:logger] = Logger.new(@rye_debug) if @rye_debug # Enable Net::SSH debugging - @rye_opts[:paranoid] = true unless @rye_opts[:safe] == false # See Net::SSH.start + @rye_opts[:paranoid] = true unless @rye_safe == false # See Net::SSH.start # Add the given private keys to the keychain that will be used for @rye_host add_keys(@rye_opts[:keys]) # We don't want Net::SSH to handle the keypairs. This may change @@ -167,61 +169,10 @@ @rye_current_umask = val self end - # Open an SSH session with +@rye_host+. This called automatically - # when you the first comamnd is run if it's not already connected. - # Raises a Rye::NoHost exception if +@rye_host+ is not specified. - # Will attempt a password login up to 3 times if the initial - # authentication fails. - # * +reconnect+ Disconnect first if already connected. The default - # is true. When set to false, connect will do nothing if already - # connected. - def connect(reconnect=true) - raise Rye::NoHost unless @rye_host - return if @rye_ssh && !reconnect - disconnect if @rye_ssh - debug "Opening connection to #{@rye_host} as #{@rye_opts[:user]}" - highline = HighLine.new # Used for password prompt - retried = 0 - - begin - @rye_ssh = Net::SSH.start(@rye_host, @rye_opts[:user], @rye_opts || {}) - rescue Net::SSH::HostKeyMismatch => ex - STDERR.puts ex.message - STDERR.puts "NOTE: EC2 instances generate new SSH keys on first boot." - print "\a" if @rye_info # Ring the bell - if highline.ask("Continue? ").strip.match(/\Ay|yes|sure|ya\z/i) - @rye_opts[:paranoid] = false - retry - else - raise Net::SSH::HostKeyMismatch - end - rescue Net::SSH::AuthenticationFailed => ex - print "\a" if retried == 0 && @rye_info # Ring the bell once - retried += 1 - if STDIN.tty? && retried <= 3 - STDERR.puts "Passwordless login failed for #{@rye_opts[:user]}" - @rye_opts[:password] = highline.ask("Password: ") { |q| q.echo = '' } - @rye_opts[:auth_methods] ||= [] - @rye_opts[:auth_methods] << 'password' - retry - else - raise Net::SSH::AuthenticationFailed - end - end - - # We add :auth_methods (a Net::SSH joint) to force asking for a - # password if the initial (key-based) authentication fails. We - # need to delete the key from @rye_opts otherwise it lingers until - # the next connection (if we switch_user is called for example). - @rye_opts.delete :auth_methods if @rye_opts.has_key?(:auth_methods) - - self - end - # Reconnect as another user. This is different from su= # which executes subsequent commands via +su -c COMMAND USER+. # * +newuser+ The username to reconnect as # # NOTE: if there is an open connection, it's disconnected @@ -507,10 +458,30 @@ # def batch(*args, &block) self.instance_exec *args, &block end + # Like batch, except it disables safe mode before executing the block. + # After executing the block, safe mode is returned back to whichever + # state it was previously in. In other words, this method won't enable + # safe mode if it was already disabled. + def unsafely(*args, &block) + previous_state = @rye_safe + disable_safe_mode + self.instance_exec *args, &block + @rye_safe = previous_state + end + + # See unsafely (except in reverse) + def safely(*args, &block) + previous_state = @rye_safe + enable_safe_mode + self.instance_exec *args, &block + @rye_safe = previous_state + end + + # instance_exec for Ruby 1.8 written by Mauricio Fernandez # http://eigenclass.org/hiki/instance_exec if RUBY_VERSION =~ /1.8/ module InstanceExecHelper; end include InstanceExecHelper @@ -537,10 +508,63 @@ @rye_post_command_hook = block if block @rye_post_command_hook end + + # Open an SSH session with +@rye_host+. This called automatically + # when you the first comamnd is run if it's not already connected. + # Raises a Rye::NoHost exception if +@rye_host+ is not specified. + # Will attempt a password login up to 3 times if the initial + # authentication fails. + # * +reconnect+ Disconnect first if already connected. The default + # is true. When set to false, connect will do nothing if already + # connected. + def connect(reconnect=true) + raise Rye::NoHost unless @rye_host + return if @rye_ssh && !reconnect + disconnect if @rye_ssh + debug "Opening connection to #{@rye_host} as #{@rye_opts[:user]}" + highline = HighLine.new # Used for password prompt + retried = 0 + + begin + @rye_ssh = Net::SSH.start(@rye_host, @rye_opts[:user], @rye_opts || {}) + rescue Net::SSH::HostKeyMismatch => ex + STDERR.puts ex.message + STDERR.puts "NOTE: EC2 instances generate new SSH keys on first boot." + print "\a" if @rye_info # Ring the bell + if highline.ask("Continue? ").strip.match(/\Ay|yes|sure|ya\z/i) + @rye_opts[:paranoid] = false + retry + else + raise Net::SSH::HostKeyMismatch + end + rescue Net::SSH::AuthenticationFailed => ex + print "\a" if retried == 0 && @rye_info # Ring the bell once + retried += 1 + if STDIN.tty? && retried <= 3 + STDERR.puts "Passwordless login failed for #{@rye_opts[:user]}" + @rye_opts[:password] = highline.ask("Password: ") { |q| q.echo = '' } + @rye_opts[:auth_methods] ||= [] + @rye_opts[:auth_methods] << 'password' + retry + else + raise Net::SSH::AuthenticationFailed + end + end + + # We add :auth_methods (a Net::SSH joint) to force asking for a + # password if the initial (key-based) authentication fails. We + # need to delete the key from @rye_opts otherwise it lingers until + # the next connection (if we switch_user is called for example). + @rye_opts.delete :auth_methods if @rye_opts.has_key?(:auth_methods) + + self + end + + private def debug(msg="unknown debug msg"); @rye_debug.puts msg if @rye_debug; end def error(msg="unknown error msg"); @rye_error.puts msg if @rye_error; end def pinfo(msg="unknown info msg"); @rye_info.print msg if @rye_info; end @@ -580,11 +604,11 @@ cmd, args = prep_args(*args) connect if !@rye_ssh || @rye_ssh.closed? raise Rye::NotConnected, @rye_host unless @rye_ssh && !@rye_ssh.closed? - + cmd_clean = Rye.escape(@rye_safe, cmd, args) cmd_clean = prepend_env(cmd_clean) # Add the current working directory before the command if supplied. # The command will otherwise run in the user's home directory. @@ -658,41 +682,50 @@ [cmd, args] end # Executes +command+ via SSH # Returns an Array with 4 elements: [stdout, stderr, exit code, exit signal] + # NOTE: This method needs to be replaced to fully support interactive + # commands. This implementation is weird because it's getting just STDOUT and + # STDERR responses (check value of "type"). on_data and on_extended_data method + # hooks are not used. See the following threads for implementation ideas: + # + # http://www.ruby-forum.com/topic/141814 + # http://www.ruby-forum.com/topic/169997 + # def net_ssh_exec!(command) block ||= Proc.new do |channel, type, data| channel[:stdout] ||= "" channel[:stderr] ||= "" channel[:exit_code] ||= 0 channel[:stdout] << data if type == :stdout - channel[:stderr] << data if type == :stderr + if type == :stderr + # NOTE: Use sudo to test this since it prompts for a passwords. + # Use sudo -K to kill the user's timestamp (ask for a password every time) + if data =~ /Password:/ + ret = Annoy.get_user_input("Password: ", '*') + channel.send_data "#{ret}\n" + else + channel[:stderr] << data + end + end channel.on_request("exit-status") do |ch, data| # Anything greater than 0 is an error channel[:exit_code] = data.read_long end channel.on_request("exit-signal") do |ch, data| # This should be the POSIX SIGNAL that ended the process channel[:exit_signal] = data.read_long end - # For long-running commands like top, this will print the output. - # It's cool, but we'd also need to enable STDIN to interact with - # command. - #channel.on_data do |ch, data| - # puts "got stdout: #{data}" - # #channel.send_data "something for stdin\n" - #end - # - #channel.on_extended_data do |ch, data| - # #puts "got stdout: #{data}" - # #channel.send_data "something for stdin\n" - #end end channel = @rye_ssh.exec(command, &block) + channel.wait # block until we get a response + channel.request_pty do |ch, success| + raise "Could not obtain pty (i.e. an interactive ssh session)" if !success + end channel[:exit_code] = 0 if channel[:exit_code] == nil channel[:exit_code] &&= channel[:exit_code].to_i channel[:stderr].gsub!(/bash: line \d+:\s+/, '') if channel[:stderr]