class Rye::Hop
  1. lib/rye/hop.rb
Parent: Rye

Rye::Hop

The Rye::Hop class represents a machine. This class allows boxes to by accessed via it.

rhop = Rye::Hop.new('firewall.lan')
rbox = Rye::Box.new('filibuster', :via => rhop)
rbox.uptime     # => 20:53  up 1 day,  1:52, 4 users

Or

rbox = Rye::Box.new('filibuster', :via => 'firewall.lan')

Constants

MAX_PORT = 65535  

The maximum port number that the gateway will attempt to use to forward connections from.

MIN_PORT = 1024  

The minimum port number that the gateway will attempt to use to forward connections from.

Public Instance Aliases

add_key -> add_keys
remove_key -> remove_keys

Public Class methods

new (host, opts={})
  • host The hostname to connect to. Default: localhost.

  • user The username to connect as. Default: SSH config file or current shell user.

  • opts a hash of optional arguments.

The opts hash excepts the following keys:

  • :port => remote server ssh port. Default: SSH config file or 22

  • :keys => one or more private key file paths (passwordless login)

  • :via => the Rye::Hop to access this host through

  • :info => an IO object to print Rye::Box command info to. Default: nil

  • :debug => an IO object to print Rye::Box debugging info to. Default: nil

  • :error => an IO object to print Rye::Box errors to. Default: STDERR

  • :getenv => pre-fetch host environment variables? (default: true)

  • :password => the user's password (ignored if there's a valid private key)

  • :templates => the template engine to use for uploaded files. One of: :erb (default)

  • :sudo => Run all commands via sudo (default: false)

NOTE: opts can also contain any parameter supported by Net::SSH.start that is not already mentioned above.

[show source]
# File lib/rye/hop.rb, line 81
def initialize(host, opts={})
  ssh_opts = ssh_config_options(host)
  @rye_exception_hook = {}
  @rye_host = host
  
  if opts[:user]
    @rye_user = opts[:user]
  else
    @rye_user = ssh_opts[:user] || Rye.sysinfo.user
  end
  
  # These opts are use by Rye::Box and also passed to
  # Net::SSH::Gateway (and Net::SSH)
  @rye_opts = {
    :port => ssh_opts[:port],
    :keys => Rye.keys,
    :via => nil,
    :info => nil,
    :debug => nil,
    :error => STDERR,
    :getenv => true,
    :templates => :erb,
    :quiet => false
  }.merge(opts)

  @next_port = MAX_PORT

  # Close the SSH session before Ruby exits. This will do nothing
  # if disconnect has already been called explicitly. 
  at_exit { self.disconnect }

  # Properly handle whether the opt :via is a +Rye::Hop+ or a +String+
  # and does nothing if nil
  via_hop(@rye_opts.delete(:via))

  # @rye_opts gets sent to Net::SSH so we need to remove the keys
  # that are not meant for it. 
  @rye_safe, @rye_debug = @rye_opts.delete(:safe), @rye_opts.delete(:debug)
  @rye_info, @rye_error = @rye_opts.delete(:info), @rye_opts.delete(:error)
  @rye_getenv = {} if @rye_opts.delete(:getenv) # Enable getenv with a hash
  @rye_ostype, @rye_impltype = @rye_opts.delete(:ostype), @rye_opts.delete(:impltype)
  @rye_quiet, @rye_sudo = @rye_opts.delete(:quiet), @rye_opts.delete(:sudo)
  @rye_templates = @rye_opts.delete(:templates)
  
  # Store the state of the terminal 
  @rye_stty_save = `stty -g`.chomp rescue nil
  
  unless @rye_templates.nil?
    require @rye_templates.to_s   # should be :erb
  end
  
  @rye_opts[:logger] = Logger.new(@rye_debug) if @rye_debug # Enable Net::SSH debugging
  @rye_opts[:keys] = [@rye_opts[:keys]].flatten.compact
  
  # Just in case someone sends a true value rather than IO object
  @rye_debug = STDERR if @rye_debug == true || DEBUG
  @rye_error = STDERR if @rye_error == true
  @rye_info = STDOUT if @rye_info == true
  
  # Add the given private keys to the keychain that will be used for @rye_host
  add_keys(@rye_opts[:keys])
  
  # From: capistrano/lib/capistrano/cli.rb
  STDOUT.sync = true # so that Net::SSH prompts show up
  
  debug "ssh-agent info: #{Rye.sshagent_info.inspect}"
  debug @rye_opts.inspect
end

Public Instance methods

== (other)

Compares itself with the other box. If the hostnames are the same, this will return true. Otherwise false.

[show source]
# File lib/rye/hop.rb, line 361
def ==(other)
  @rye_host == other.host
end
add_keys (*keys)

Add one or more private keys to the list of key paths.

  • keys is a list of file paths to private keys

Returns the instance of Box

[show source]
# File lib/rye/hop.rb, line 204
def add_keys(*keys)
  @rye_opts[:keys] ||= []
  @rye_opts[:keys] += keys.flatten.compact
  @rye_opts[:keys].uniq!
  self # MUST RETURN self
end
connect (reconnect=true)

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.

[show source]
# File lib/rye/hop.rb, line 246
def connect(reconnect=true)
  raise Rye::NoHost unless @rye_host
  return if @rye_ssh && !reconnect
  disconnect if @rye_ssh 
  if @rye_via
    debug "Opening connection to #{@rye_host} as #{@rye_user}, via #{@rye_via.host}"
  else
    debug "Opening connection to #{@rye_host} as #{@rye_user}"
  end
  highline = HighLine.new # Used for password prompt
  retried = 0
  @rye_opts[:keys].compact!  # A quick fix in Windows. TODO: Why is there a nil?
  begin
    if @rye_via
      # tell the +Rye::Hop+ what and where to setup,
      # it returns the local port used
      @rye_localport = @rye_via.fetch_port(@rye_host, @rye_opts[:port].nil? ? 22 : @rye_opts[:port] )
      @rye_ssh = Net::SSH.start("localhost", @rye_user, @rye_opts.merge(:port => @rye_localport) || {}) 
    else
      @rye_ssh = Net::SSH.start(@rye_host, @rye_user, @rye_opts || {}) 
    end
    debug "starting the port forward thread"
    port_loop
  rescue Net::SSH::HostKeyMismatch => ex
    STDERR.puts ex.message
    print "\a" if @rye_info # Ring the bell
    if highline.ask("Continue? ").strip.match(/\Ay|yes|sure|ya\z/)
      @rye_opts[:paranoid] = false
      retry
    else
      raise ex
    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_user}"
      @rye_opts[:password] = highline.ask("Password: ") { |q| q.echo = '' }.strip
      @rye_opts[:auth_methods] ||= []
      @rye_opts[:auth_methods].push *['keyboard-interactive', 'password']
      retry
    else
      raise ex
    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
debug? ()
[show source]
# File lib/rye/hop.rb, line 54
def debug?; !@rye_debug.nil?; end
disconnect ()

Close the SSH session with +@rye_host+. This is called automatically at exit if the connection is open.

[show source]
# File lib/rye/hop.rb, line 317
def disconnect
  return unless @rye_ssh && !@rye_ssh.closed?
  begin
    debug "removing active forwards"
    remove_hops!
    debug "killing port_loop @rye_port_thread"
    @rye_port_thread.kill
    if @rye_ssh.busy?;
      info "Is something still running? (ctrl-C to exit)"
      Timeout::timeout(10) do
        @rye_ssh.loop(0.3) { @rye_ssh.busy?; }
      end
    end
    debug "Closing connection to #{@rye_ssh.host}"
    @rye_ssh.close
    if @rye_via
      debug "disconnecting Hop #{@rye_via.host}"
      @rye_via.disconnect
    end
  rescue SystemCallError, Timeout::Error => ex
    error "Rye::Hop: Disconnect timeout (#{ex.message})"
    debug ex.backtrace
  rescue Interrupt
    debug "Exiting..."
  end
end
error? ()
[show source]
# File lib/rye/hop.rb, line 55
def error?; !@rye_error.nil?; end
exception_hook= (val)

A Hash. The keys are exception classes, the values are Procs to execute

[show source]
# File lib/rye/hop.rb, line 59
def exception_hook=(val); @rye_exception_hook = val; end
fetch_port (host, port = 22, localport = nil)

instance method, that will setup a forward, and return the port used

[show source]
# File lib/rye/hop.rb, line 181
def fetch_port(host, port = 22, localport = nil)
  connect unless @rye_ssh
  if localport.nil?
    port_used = next_port
  else
    port_used = localport
  end
  # i would like to check if the port and host 
  # are already an active_locals forward, but that 
  # info does not get returned, and trusting the localport
  # is not enough information, so lets just set up a new one
  @rye_ssh.forward.local(port_used, host, port)
  return port_used
end
host ()
[show source]
# File lib/rye/hop.rb, line 39
def host; @rye_host; end
host= (val)
[show source]
# File lib/rye/hop.rb, line 48
def host=(val); @rye_host = val; end
host_key ()

Returns the host SSH keys for this box

[show source]
# File lib/rye/hop.rb, line 366
def host_key
  raise "No host" unless @rye_host
  Rye.remote_host_keys(@rye_host)
end
info? ()
[show source]
# File lib/rye/hop.rb, line 53
def info?; !@rye_info.nil?; end
inspect ()
[show source]
# File lib/rye/hop.rb, line 350
def inspect
  %{#<%s:%s name=%s cwd=%s umask=%s env=%s via=%s opts=%s keys=%s>} % 
  [self.class.to_s, self.host, self.nickname,
   @rye_current_working_directory, @rye_current_umask,
   (@rye_current_environment_variables || '').inspect,
   (@rye_via || '').inspect,
   self.opts.inspect, self.keys.inspect]
end
keys ()

See Rye.keys

[show source]
# File lib/rye/hop.rb, line 345
def keys; Rye.keys; end
nickname ()
[show source]
# File lib/rye/hop.rb, line 44
def nickname; @rye_nickname || host; end
nickname= (val)
[show source]
# File lib/rye/hop.rb, line 47
def nickname=(val); @rye_nickname = val; end
opts ()
[show source]
# File lib/rye/hop.rb, line 40
def opts; @rye_opts; end
opts= (val)
[show source]
# File lib/rye/hop.rb, line 49
def opts=(val); @rye_opts = val; end
remove_hops! ()

Cancel the port forward on all active local forwards

[show source]
# File lib/rye/hop.rb, line 302
def remove_hops!
  return unless @rye_ssh && @rye_ssh.forward.active_locals.count > 0
  @rye_ssh.forward.active_locals.each {|fport, fhost| 
    @rye_ssh.forward.cancel_local(fport, fhost)
  }
  if !@rye_ssh.channels.empty?
    @rye_ssh.channels.each {|channel|
      channel[-1].close
    }
  end
  return @rye_ssh.forward.active_locals.count
end
remove_keys (*keys)

Remove one or more private keys fromt he list of key paths.

  • keys is a list of file paths to private keys

Returns the instance of Box

[show source]
# File lib/rye/hop.rb, line 215
def remove_keys(*keys)
  @rye_opts[:keys] ||= []
  @rye_opts[:keys] -= keys.flatten.compact
  @rye_opts[:keys].uniq!
  self # MUST RETURN self
end
root? ()
[show source]
# File lib/rye/hop.rb, line 42
def root?; user.to_s == "root" end
ssh_config_options (host)

Parse SSH config files for use with Net::SSH

[show source]
# File lib/rye/hop.rb, line 197
def ssh_config_options(host)
  return Net::SSH::Config.for(host)
end
switch_user (newuser)

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 but not reconnected because it's possible it wasn't connected yet in the first place (if you create the instance with default settings for example)

[show source]
# File lib/rye/hop.rb, line 231
def switch_user(newuser)
  return if newuser.to_s == self.user.to_s
  @rye_opts ||= {}
  @rye_user = newuser
  disconnect
end
to_s ()

Returns +user@rye_host+

[show source]
# File lib/rye/hop.rb, line 348
def to_s; '%s@rye_%s' % [user, @rye_host]; end
user ()
[show source]
# File lib/rye/hop.rb, line 41
def user; @rye_user; end
via ()
[show source]
# File lib/rye/hop.rb, line 45
def via; @rye_via; end
via? ()
[show source]
# File lib/rye/hop.rb, line 52
def via?; !@rye_via.nil?; end
via_hop (*hops)
  • hops Rye::Hop objects will be added directly

to the set. Hostnames will be used to create new instances of Rye::Hop h1 = Rye::Hop.new "host1" h1.via_hop "host2", :user => "service_user"

OR

h1 = Rye::Hop.new "host1" h2 = Rye::Hop.new "host2" h1.via_hop h2

[show source]
# File lib/rye/hop.rb, line 161
def via_hop(*hops)
  hops = hops.flatten.compact 
  if hops.first.nil?
    return @rye_via
  elsif hops.first.is_a?(Rye::Hop)
    @rye_via = hops.first
  elsif hops.first.is_a?(String)
    hop = hops.shift
    if hops.first.is_a?(Hash)
      @rye_via = Rye::Hop.new(hop, hops.first)
    else
      @rye_via = Rye::Hop.new(hop)
    end
  end
  disconnect
  self
end