Class: Rye::Hop

Inherits:
Object
  • Object
show all
Defined in:
lib/rye/hop.rb

Overview

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')

  • When anything confusing happens, enable debug in initialize

by passing :debug => STDERR. This will output Rye debug info as well as Net::SSH info. This is VERY helpful for figuring out why some command is hanging or otherwise acting weird.

  • If a remote command is hanging, it’s probably because a

Net::SSH channel is waiting on_extended_data (a prompt). ++

Constant Summary collapse

MAX_PORT =

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

65535
MIN_PORT =

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

1024

Instance Method Summary collapse

Constructor Details

#initialize(host, opts = {}) ⇒ Hop

  • 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.



81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
# 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

Instance Method Details

#==(other) ⇒ Object

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



361
362
363
# File 'lib/rye/hop.rb', line 361

def ==(other)
  @rye_host == other.host
end

#add_keys(*keys) ⇒ Object Also known as: add_key

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



204
205
206
207
208
209
# 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) ⇒ Object

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.

Raises:



246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
# 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/i)
      @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?Boolean

Returns:

  • (Boolean)


54
# File 'lib/rye/hop.rb', line 54

def debug?; !@rye_debug.nil?; end

#disconnectObject

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



317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
# 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?Boolean

Returns:

  • (Boolean)


55
# File 'lib/rye/hop.rb', line 55

def error?; !@rye_error.nil?; end

#exception_hook=(val) ⇒ Object

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



59
# File 'lib/rye/hop.rb', line 59

def exception_hook=(val); @rye_exception_hook = val; end

#fetch_port(host, port = 22, localport = nil) ⇒ Object

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



181
182
183
184
185
186
187
188
189
190
191
192
193
194
# 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

#hostObject



39
# File 'lib/rye/hop.rb', line 39

def host; @rye_host; end

#host=(val) ⇒ Object



48
# File 'lib/rye/hop.rb', line 48

def host=(val); @rye_host = val; end

#host_keyObject

Returns the host SSH keys for this box



366
367
368
369
# File 'lib/rye/hop.rb', line 366

def host_key
  raise "No host" unless @rye_host
  Rye.remote_host_keys(@rye_host)
end

#info?Boolean

Returns:

  • (Boolean)


53
# File 'lib/rye/hop.rb', line 53

def info?; !@rye_info.nil?; end

#inspectObject



350
351
352
353
354
355
356
357
# File 'lib/rye/hop.rb', line 350

def inspect
  %q{#<%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

#keysObject

See Rye.keys



345
# File 'lib/rye/hop.rb', line 345

def keys; Rye.keys; end

#nicknameObject



44
# File 'lib/rye/hop.rb', line 44

def nickname; @rye_nickname || host; end

#nickname=(val) ⇒ Object



47
# File 'lib/rye/hop.rb', line 47

def nickname=(val); @rye_nickname = val; end

#optsObject



40
# File 'lib/rye/hop.rb', line 40

def opts; @rye_opts; end

#opts=(val) ⇒ Object



49
# File 'lib/rye/hop.rb', line 49

def opts=(val); @rye_opts = val; end

#remove_hops!Object

Cancel the port forward on all active local forwards



302
303
304
305
306
307
308
309
310
311
312
313
# 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) ⇒ Object Also known as: remove_key

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



215
216
217
218
219
220
# 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?Boolean

Returns:

  • (Boolean)


42
# File 'lib/rye/hop.rb', line 42

def root?; user.to_s == "root" end

#ssh_config_options(host) ⇒ Object

Parse SSH config files for use with Net::SSH



197
198
199
# File 'lib/rye/hop.rb', line 197

def ssh_config_options(host)
  return Net::SSH::Config.for(host)
end

#switch_user(newuser) ⇒ Object

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)



231
232
233
234
235
236
# 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_sObject

Returns user@rye_host



348
# File 'lib/rye/hop.rb', line 348

def to_s; '%s@rye_%s' % [user, @rye_host]; end

#userObject



41
# File 'lib/rye/hop.rb', line 41

def user; @rye_user; end

#viaObject



45
# File 'lib/rye/hop.rb', line 45

def via; @rye_via; end

#via?Boolean

Returns:

  • (Boolean)


52
# File 'lib/rye/hop.rb', line 52

def via?; !@rye_via.nil?; end

#via_hop(*hops) ⇒ Object

  • 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



161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
# 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