Class: Rye::Box

Inherits:
Object
  • Object
show all
Includes:
Cmd
Defined in:
lib/rye/box.rb

Overview

Rye::Box

The Rye::Box class represents a machine. All system commands are made through this class.

rbox = Rye::Box.new('filibuster')
rbox.hostname   # => filibuster
rbox.uname      # => FreeBSD
rbox.uptime     # => 20:53  up 1 day,  1:52, 4 users

You can also run local commands through SSH

rbox = Rye::Box.new('localhost') 
rbox.hostname   # => localhost
rbox.uname(:a)  # => Darwin vanya 9.6.0 ...

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from Cmd

#awk, #bash, #cat, #chmod, #cp, #cvs, #date, #df, #du, #echo, #env, #exists?, #git, #grep, #history, #hostname, #ls, #mkdir, #mkfs, #mount, #mv, #perl, #printenv, #ps, #pwd, #python, #ruby, #sed, #sh, #sleep, #sudo, #svn, #test, #touch, #umount, #uname, #uptime, #wc

Constructor Details

#initialize(host = 'localhost', opts = {}) ⇒ Box

  • host The hostname to connect to. The default is localhost.

  • opts a hash of optional arguments.

The opts hash excepts the following keys:

  • :user => the username to connect as. Default: the current user.

  • :safe => should Rye be safe? Default: true

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

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

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

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



54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
# File 'lib/rye/box.rb', line 54

def initialize(host='localhost', opts={})
  
  # These opts are use by Rye::Box and also passed to Net::SSH
  @opts = {
    :user => Rye.sysinfo.user, 
    :safe => true,
    :port => 22,
    :keys => [],
    :debug => nil,
    :error => STDERR,
  }.merge(opts)
  
  # See Net::SSH.start
  @opts[:paranoid] = true unless @opts[:safe] == false
  
  # Close the SSH session before Ruby exits. This will do nothing
  # if disconnect has already been called explicitly. 
  at_exit {
    self.disconnect
  }
        
  @host = host
  
  @safe = @opts.delete(:safe)
  @debug = @opts.delete(:debug)
  @error = @opts.delete(:error)
  
  debug @opts.inspect
        
  add_keys(@opts[:keys])
  
  # We don't want Net::SSH to handle the keypairs. This may change
  # but for we're letting ssh-agent do it. 
  #@opts.delete(:keys)
  

  debug "ssh-agent info: #{Rye.sshagent_info.inspect}"
  
end

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing(meth, *args, &block) ⇒ Object

A handler for undefined commands. Raises Rye::CommandNotFound exception.



308
309
310
# File 'lib/rye/box.rb', line 308

def method_missing(meth, *args, &block)
  raise Rye::CommandNotFound, "#{meth.to_s} (args: #{args.join(' ')})"
end

Instance Attribute Details

#current_working_directoryObject (readonly)

The most recent value from Box.cd or Box.[]



37
38
39
# File 'lib/rye/box.rb', line 37

def current_working_directory
  @current_working_directory
end

#hostObject

Returns the value of attribute host.



31
32
33
# File 'lib/rye/box.rb', line 31

def host
  @host
end

#optsObject

Returns the value of attribute opts.



34
35
36
# File 'lib/rye/box.rb', line 34

def opts
  @opts
end

#safeObject

Returns the value of attribute safe.



33
34
35
# File 'lib/rye/box.rb', line 33

def safe
  @safe
end

#sshObject (readonly)

An instance of Net::SSH::Connection::Session



26
27
28
# File 'lib/rye/box.rb', line 26

def ssh
  @ssh
end

Instance Method Details

#==(other) ⇒ Object

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



253
254
255
# File 'lib/rye/box.rb', line 253

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

#[](key = nil) ⇒ Object

Change the current working directory (sort of).

I haven’t been able to wrangle Net::SSH to do my bidding. “My bidding” in this case, is maintaining an open channel between commands. I’m using Net::SSH::Connection::Session#exec for all commands which is like a funky helper method that opens a new channel each time it’s called. This seems to be okay for one-off commands but changing the directory only works for the channel it’s executed in. The next time exec is called, there’s a new channel which is back in the default (home) directory.

Long story short, the work around is to maintain the current directory locally and send it with each command.

rbox.pwd              # => /home/rye ($ pwd )
rbox['/usr/bin'].pwd  # => /usr/bin  ($ cd /usr/bin && pwd)
rbox.pwd              # => /usr/bin  ($ cd /usr/bin && pwd)


122
123
124
125
# File 'lib/rye/box.rb', line 122

def [](key=nil)
  @current_working_directory = key
  self
end

#add_env(n, v) ⇒ Object Also known as: add_environment_variable

Add an environment variable. n and v are the name and value. Returns the instance of Rye::Box



223
224
225
226
227
# File 'lib/rye/box.rb', line 223

def add_env(n, v)
  debug "Added env: #{n}=#{v}"
  (@current_environment_variables ||= {})[n] = v
  self
end

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

Add one or more private keys to the SSH Agent.

  • additional_keys is a list of file paths to private keys

Returns the instance of Box



208
209
210
211
212
213
214
215
216
217
218
# File 'lib/rye/box.rb', line 208

def add_keys(*additional_keys)
  additional_keys = [additional_keys].flatten.compact || []
  return if additional_keys.empty?
  ret = Rye.add_keys(additional_keys)
  if ret.is_a?(Rye::Rap)
    debug "ssh-add exit_code: #{ret.exit_code}" 
    debug "ssh-add stdout: #{ret.stdout}"
    debug "ssh-add stderr: #{ret.stderr}"
  end
  self #MUST RETURN itself
end

#authorize_keysObject

Copy the local public keys (as specified by Rye.keys) to this box into ~/.ssh/authorized_keys and ~/.ssh/authorized_keys2. Returns an Array of the private keys files used to generate the public keys.

NOTE: authorize_keys disables safe-mode for this box while it runs which will hit you funky style if your using a single instance of Rye::Box in a multithreaded situation.



271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
# File 'lib/rye/box.rb', line 271

def authorize_keys
  added_keys = []
  @safe= false
  Rye.keys.each do |key|
    path = key[2]
    debug "# Public key for #{path}"
    k = Rye::Key.from_file(path).public_key.to_ssh2
    self.mkdir(:p, :m, '700', '$HOME/.ssh') # Silently create dir if it doesn't exist
    self.echo("'#{k}' >> $HOME/.ssh/authorized_keys")
    self.echo("'#{k}' >> $HOME/.ssh/authorized_keys2")
    self.chmod('-R', '0600', '$HOME/.ssh/authorized_keys*')
    added_keys << path
  end
  @safe = true
  added_keys
end

#authorize_keys_localObject

Authorize the current user to login to the local machine via SSH without a password. This is the same functionality as authorize_keys except run with local shell commands.



291
292
293
294
295
296
297
298
299
300
301
302
303
304
# File 'lib/rye/box.rb', line 291

def authorize_keys_local
  added_keys = []
  Rye.keys.each do |key|
    path = key[2]
    debug "# Public key for #{path}"
    k = Rye::Key.from_file(path).public_key.to_ssh2
    Rye.shell(:mkdir, :p, :m, '700', '$HOME/.ssh') # Silently create dir if it doesn't exist
    Rye.shell(:echo, "'#{k}' >> $HOME/.ssh/authorized_keys")
    Rye.shell(:echo, "'#{k}' >> $HOME/.ssh/authorized_keys2")
    Rye.shell(:chmod, '-R', '0600', '$HOME/.ssh/authorized_keys*')
    added_keys << path
  end
  added_keys
end

#canObject Also known as: commands, cmds

Returns an Array of system commands available over SSH



96
97
98
# File 'lib/rye/box.rb', line 96

def can
  Rye::Cmd.instance_methods
end

#cd(key = nil) ⇒ Object

alias :cd :‘[]’ # fix for jruby



127
128
129
130
# File 'lib/rye/box.rb', line 127

def cd(key=nil); 
  @current_working_directory = key
  self
end

#connectObject

Open an SSH session with @host. This called automatically when you the first comamnd is run if it’s not already connected. Raises a Rye::NoHost exception if @host is not specified. Will attempt a password login up to 3 times if the initial authentication fails.

Raises:



137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
# File 'lib/rye/box.rb', line 137

def connect
  raise Rye::NoHost unless @host
  disconnect if @ssh 
  debug "Opening connection to #{@host} as #{@opts[:user]}"
  highline = HighLine.new # Used for password prompt
  retried = 0
  
  begin
    @ssh = Net::SSH.start(@host, @opts[:user], @opts || {}) 
  rescue Net::SSH::AuthenticationFailed => ex
    retried += 1
    if STDIN.tty? && retried <= 3
      @opts[:password] = highline.ask("Password: ") { |q| q.echo = '' }
      @opts[:auth_methods] ||= []
      @opts[:auth_methods] << 'password'
      retry
    else
      STDERR.puts "Authentication failed."
      exit 0
    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 @opts otherwise it lingers until
  # the next connection (if we switch_user is called for example).
  @opts.delete :auth_methods if @opts.has_key?(:auth_methods)
  
  @ssh.is_a?(Net::SSH::Connection::Session) && !@ssh.closed?
  self
end

#disconnectObject

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



171
172
173
174
175
176
# File 'lib/rye/box.rb', line 171

def disconnect
  return unless @ssh && !@ssh.closed?
  @ssh.loop(0.1) { @ssh.busy? }
  debug "Closing connection to #{@ssh.host}"
  @ssh.close
end

#host_keyObject

Returns the host SSH keys for this box



258
259
260
261
# File 'lib/rye/box.rb', line 258

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

#inspectObject



244
245
246
247
248
249
# File 'lib/rye/box.rb', line 244

def inspect
  %q{#<%s:%s cwd=%s env=%s safe=%s opts=%s>} % 
  [self.class.to_s, self.host, 
   @current_working_directory, (@current_environment_variables || '').inspect,
   self.safe, self.opts.inspect]
end

#interactive_ssh(run = true) ⇒ Object

Open an interactive SSH session. This only works if STDIN.tty? returns true. Otherwise it returns the SSH command that would have been run. This requires the SSH command-line executable (ssh).

  • run when set to false, it will return the SSH command as a String

and not open an SSH session.



197
198
199
200
201
202
203
# File 'lib/rye/box.rb', line 197

def interactive_ssh(run=true)
  debug "interactive_ssh with keys: #{Rye.keys.inspect}"
  run = false unless STDIN.tty?      
  cmd = Rye.prepare_command("ssh", "#{@opts[:user]}@#{@host}")
  return cmd unless run
  system(cmd)
end

#keysObject

See Rye.keys



235
236
237
# File 'lib/rye/box.rb', line 235

def keys
  Rye.keys
end

#preview_command(*args) ⇒ Object



311
312
313
# File 'lib/rye/box.rb', line 311

def preview_command(*args)
  prep_args(*args).join(' ')
end

#switch_user(newuser) ⇒ Object

Reconnect as another user

  • newuser The username to reconnect as

NOTE: if there is an open connection, it’s disconnected and a new one is opened for the given user.



183
184
185
186
187
188
189
# File 'lib/rye/box.rb', line 183

def switch_user(newuser)
  return if newuser.to_s == self.user.to_s
  @opts ||= {}
  @opts[:user] = newuser
  disconnect
  connect
end

#to_sObject

Returns @host



240
241
242
# File 'lib/rye/box.rb', line 240

def to_s
  @host
end

#userObject



230
231
232
# File 'lib/rye/box.rb', line 230

def user
  (@opts || {})[:user]
end