lib/rye/box.rb in rye-0.6.6 vs lib/rye/box.rb in rye-0.7.0
- old
+ new
@@ -1,10 +1,9 @@
module Rye
-
# = Rye::Box
#
# The Rye::Box class represents a machine. All system
# commands are made through this class.
#
@@ -28,29 +27,32 @@
# Net::SSH channel is waiting on_extended_data (a prompt).
#++
class Box
include Rye::Cmd
- # An instance of Net::SSH::Connection::Session
- attr_reader :ssh
-
- attr_reader :info
- attr_reader :debug
- attr_reader :error
-
- attr_accessor :host
- attr_accessor :safe
- attr_accessor :opts
-
- # The most recent value from Box.cd or Box.[]
- attr_reader :current_working_directory
- # The most recent valud for umask (or 0022)
- attr_reader :current_umask
-
- attr_writer :post_command_hook
- attr_writer :pre_command_hook
-
+ def host; @rye_host; end
+ def opts; @rye_opts; end
+ def safe; @rye_safe; end
+
+ def host=(val); @rye_host = val; end
+ def opts=(val); @rye_opts = val; end
+ def safe=(val); @rye_safe = val; 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
+
+ def ssh; @rye_ssh; end
+ def info; @rye_info; end
+ def debug; @rye_debug; end
+ def error; @rye_error; end
+
+ def pre_command_hook=(val); @rye_pre_command_hook = val; end
+ def post_command_hook=(val); @rye_post_command_hook = val; end
+
# * +host+ The hostname to connect to. The default is localhost.
# * +opts+ a hash of optional arguments.
#
# The +opts+ hash excepts the following keys:
#
@@ -65,14 +67,14 @@
#
# NOTE: +opts+ can also contain any parameter supported by
# Net::SSH.start that is not already mentioned above.
#
def initialize(host='localhost', opts={})
- @host = host
+ @rye_host = host
# These opts are use by Rye::Box and also passed to Net::SSH
- @opts = {
+ @rye_opts = {
:user => Rye.sysinfo.user,
:safe => true,
:port => 22,
:keys => [],
:info => nil,
@@ -83,37 +85,37 @@
# Close the SSH session before Ruby exits. This will do nothing
# if disconnect has already been called explicitly.
at_exit { self.disconnect }
- # @opts gets sent to Net::SSH so we need to remove the keys
+ # @rye_opts gets sent to Net::SSH so we need to remove the keys
# that are not meant for it.
- @safe, @debug = @opts.delete(:safe), @opts.delete(:debug)
- @info, @error = @opts.delete(:info), @opts.delete(:error)
- @getenv = {} if @opts.delete(:getenv) # Enable getenv with a hash
+ @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
# Just in case someone sends a true value rather than IO object
- @debug = STDERR if @debug == true
- @error = STDERR if @error == true
- @info = STDOUT if @info == true
+ @rye_debug = STDERR if @rye_debug == true
+ @rye_error = STDERR if @rye_error == true
+ @rye_info = STDOUT if @rye_info == true
- @opts[:logger] = Logger.new(@debug) if @debug # Enable Net::SSH debugging
- @opts[:paranoid] = true unless @opts[:safe] == false # See Net::SSH.start
+ @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
- # Add the given private keys to the keychain that will be used for @host
- add_keys(@opts[:keys])
+ # 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
# but for we're letting ssh-agent do it.
# TODO: Check if this should ot should not be enabled.
- @opts.delete(:keys)
+ @rye_opts.delete(: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 @opts.inspect
+ debug @rye_opts.inspect
end
# Change the current working directory (sort of).
@@ -133,76 +135,76 @@
# rbox.pwd # => /home/rye ($ pwd )
# rbox['/usr/bin'].pwd # => /usr/bin ($ cd /usr/bin && pwd)
# rbox.pwd # => /usr/bin ($ cd /usr/bin && pwd)
#
def [](key=nil)
- @current_working_directory = key
+ @rye_current_working_directory = key
self
end
# Like [] except it returns an empty Rye::Rap object to mimick
# a regular command method. Call with nil key (or no arg) to
# reset.
def cd(key=nil)
- @current_working_directory = key
+ @rye_current_working_directory = key
ret = Rye::Rap.new(self)
end
# Change the current umask (sort of -- works the same way as cd)
# The default umask is 0022
def umask=(val='0022')
- @current_umask = val
+ @rye_current_umask = val
self
end
- # Open an SSH session with +@host+. This called automatically
+ # 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 +@host+ is not specified.
+ # 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 @host
- return if @ssh && !reconnect
- disconnect if @ssh
- debug "Opening connection to #{@host} as #{@opts[:user]}"
+ 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
- @ssh = Net::SSH.start(@host, @opts[:user], @opts || {})
+ @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 @info # Ring the bell
+ print "\a" if @rye_info # Ring the bell
if highline.ask("Continue? ").strip.match(/\Ay|yes|sure|ya\z/i)
- @opts[:paranoid] = false
+ @rye_opts[:paranoid] = false
retry
else
raise Net::SSH::HostKeyMismatch
end
rescue Net::SSH::AuthenticationFailed => ex
- print "\a" if retried == 0 && @info # Ring the bell once
+ print "\a" if retried == 0 && @rye_info # Ring the bell once
retried += 1
if STDIN.tty? && retried <= 3
- STDERR.puts "Passwordless login failed for #{@opts[:user]}"
- @opts[:password] = highline.ask("Password: ") { |q| q.echo = '' }
- @opts[:auth_methods] ||= []
- @opts[:auth_methods] << 'password'
+ 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 @opts otherwise it lingers until
+ # need to delete the key from @rye_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)
+ @rye_opts.delete :auth_methods if @rye_opts.has_key?(:auth_methods)
self
end
# Reconnect as another user. This is different from su=
@@ -211,24 +213,24 @@
#
# NOTE: if there is an open connection, it's disconnected
# and a new one is opened for the given user.
def switch_user(newuser)
return if newuser.to_s == self.user.to_s
- @opts ||= {}
- @opts[:user] = newuser
+ @rye_opts ||= {}
+ @rye_opts[:user] = newuser
disconnect
connect
end
- # Close the SSH session with +@host+. This is called
+ # Close the SSH session with +@rye_host+. This is called
# automatically at exit if the connection is open.
def disconnect
- return unless @ssh && !@ssh.closed?
- @ssh.loop(0.1) { @ssh.busy? }
- debug "Closing connection to #{@ssh.host}"
- @ssh.close
+ return unless @rye_ssh && !@rye_ssh.closed?
+ @rye_ssh.loop(0.1) { @rye_ssh.busy? }
+ debug "Closing connection to #{@rye_ssh.host}"
+ @rye_ssh.close
end
# Open an interactive SSH session. This only works if STDIN.tty?
# returns true. Otherwise it returns the SSH command that would
@@ -237,11 +239,11 @@
# and not open an SSH session.
#
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}")
+ cmd = Rye.prepare_command("ssh", "#{@rye_opts[:user]}@rye_#{@rye_host}")
return cmd unless run
system(cmd)
end
# Add one or more private keys to the SSH Agent.
@@ -258,59 +260,94 @@
end
self #MUST RETURN itself
end
alias :add_key :add_keys
+ # Return the value of uname in lowercase
+ # This is a temporary fix. We can use SysInfo for this, upload
+ # it, execute it directly, parse the output.
+ def ostype
+ return @rye_ostype if @rye_ostype # simple cache
+ os = self.uname.first rescue nil
+ os ||= 'unknown'
+ os &&= os.downcase
+ @rye_ostype = os
+ end
+
+ # Returns the hash containing the parsed output of "env" on the
+ # remote machine. If the initialize option +:getenv+ was set to
+ # false, this will return an empty hash.
+ # This is a lazy loaded method so it fetches the remote envvars
+ # the first time this method is called.
+ #
+ # puts rbox.getenv['HOME'] # => "/home/gloria" (remote)
+ #
+ # NOTE: This method should not raise an exception under normal
+ # circumstances.
+ #
+ def getenv
+ if @rye_getenv && @rye_getenv.empty? && self.can?(:env)
+ env = self.env rescue []
+ env.each do |nv|
+ # Parse "GLORIA_HOME=/gloria/lives/here" into a name/value
+ # pair. The regexp ensures we split only at the 1st = sign
+ n, v = nv.scan(/\A([\w_-]+?)=(.+)\z/).flatten
+ @rye_getenv[n] = v
+ end
+ end
+ @rye_getenv
+ end
+
# Add an environment variable. +n+ and +v+ are the name and value.
# Returns the instance of Rye::Box
def setenv(n, v)
debug "Adding env: #{n}=#{v}"
- debug "prev value: #{@getenv[n]}"
- @getenv[n] = v
- (@current_environment_variables ||= {})[n] = v
+ debug "prev value: #{@rye_getenv[n]}"
+ @rye_getenv[n] = v
+ (@rye_current_environment_variables ||= {})[n] = v
self
end
alias :add_env :setenv # deprecated?
# The name of the user that opened the SSH connection
- def user; (@opts || {})[:user]; end
+ def user; (@rye_opts || {})[:user]; end
# See Rye.keys
def keys; Rye.keys; end
- # Returns +user@host+
- def to_s; '%s@%s' % [user, @host]; end
+ # Returns +user@rye_host+
+ def to_s; '%s@rye_%s' % [user, @rye_host]; end
def inspect
%q{#<%s:%s cwd=%s umask=%s env=%s safe=%s opts=%s>} %
[self.class.to_s, self.host,
- @current_working_directory, @current_umask,
- (@current_environment_variables || '').inspect,
+ @rye_current_working_directory, @rye_current_umask,
+ (@rye_current_environment_variables || '').inspect,
self.safe, self.opts.inspect]
end
# Compares itself with the +other+ box. If the hostnames
# are the same, this will return true. Otherwise false.
def ==(other)
- @host == other.host
+ @rye_host == other.host
end
# Returns the host SSH keys for this box
def host_key
- raise "No host" unless @host
- Rye.remote_host_keys(@host)
+ raise "No host" unless @rye_host
+ Rye.remote_host_keys(@rye_host)
end
# Uses the output of "useradd -D" to determine the default home
# directory. This returns a GUESS rather than the a user's real
# home directory. Currently used only by authorize_keys_remote.
def guess_user_home(other_user=nil)
this_user = other_user || opts[:user]
- @guessed_homes ||= {}
+ @rye_guessed_homes ||= {}
# A simple cache.
- return @guessed_homes[this_user] if @guessed_homes.has_key?(this_user)
+ return @rye_guessed_homes[this_user] if @rye_guessed_homes.has_key?(this_user)
# Some junk to determine where user home directories are by default.
# We're relying on the command "useradd -D" so this may not work on
# different Linuxen and definitely won't work on Windows.
# This code will be abstracted out once I find a decent home for it.
@@ -335,11 +372,11 @@
n, v = nv.scan(/\A([\w_-]+?)=(.+)\z/).flatten
user_defaults[n] = v
end
end
- @guessed_homes[this_user] = "#{user_defaults['HOME']}/#{this_user}"
+ @rye_guessed_homes[this_user] = "#{user_defaults['HOME']}/#{this_user}"
end
# Copy the local public keys (as specified by Rye.keys) to
# this box into ~/.ssh/authorized_keys and ~/.ssh/authorized_keys2.
# Returns a Rye::Rap object. The private keys files used to generate
@@ -437,12 +474,12 @@
# Supply a block to be called before every command. It's called
# with three arguments: command name, an Array of arguments, user name
#
def pre_command_hook(&block)
- @pre_command_hook = block if block
- @pre_command_hook
+ @rye_pre_command_hook = block if block
+ @rye_pre_command_hook
end
# Execute a block in the context of an instance of Rye::Box.
#
# rbox = Rye::Box.new
@@ -453,39 +490,56 @@
# end
# OR
# rbox.batch(&block)
#
#
- def batch(&block)
- instance_eval &block
+ def batch(*args, &block)
+ self.instance_exec *args, &block
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
+ def instance_exec(*args, &block) # !> method redefined; discarding old instance_exec
+ mname = "__instance_exec_#{Thread.current.object_id.abs}_#{object_id.abs}"
+ InstanceExecHelper.module_eval{ define_method(mname, &block) }
+ begin
+ ret = send(mname, *args)
+ ensure
+ InstanceExecHelper.module_eval{ undef_method(mname) } rescue nil
+ end
+ ret
+ end
+ end
+
# Supply a block to be called after every command. It's called
# with one argument: an instance of Rye::Rap.
#
# When this block is supplied, the command does not raise an
# exception when the exit code is greater than 0 (the typical
# behavior) so the block needs to check the Rye::Rap object to
# determine whether an exception should be raised.
def post_command_hook(&block)
- @post_command_hook = block if block
- @post_command_hook
+ @rye_post_command_hook = block if block
+ @rye_post_command_hook
end
private
- def debug(msg="unknown debug msg"); @debug.puts msg if @debug; end
- def error(msg="unknown error msg"); @error.puts msg if @error; end
- def pinfo(msg="unknown info msg"); @info.print msg if @info; end
- def info(msg="unknown info msg"); @info.puts msg if @info; end
+ 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
+ def info(msg="unknown info msg"); @rye_info.puts msg if @rye_info; end
# Add the current environment variables to the beginning of +cmd+
def prepend_env(cmd)
- return cmd unless @current_environment_variables.is_a?(Hash)
+ return cmd unless @rye_current_environment_variables.is_a?(Hash)
env = ''
- @current_environment_variables.each_pair do |n,v|
+ @rye_current_environment_variables.each_pair do |n,v|
env << "export #{n}=#{Escape.shell_single_word(v)}; "
end
[env, cmd].join(' ')
end
@@ -511,35 +565,35 @@
def run_command(*args)
debug "run_command with keys: #{Rye.keys.inspect}"
cmd, args = prep_args(*args)
- connect if !@ssh || @ssh.closed?
- raise Rye::NotConnected, @host unless @ssh && !@ssh.closed?
+ connect if !@rye_ssh || @rye_ssh.closed?
+ raise Rye::NotConnected, @rye_host unless @rye_ssh && !@rye_ssh.closed?
- cmd_clean = Rye.escape(@safe, cmd, args)
+ 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.
- if @current_working_directory
- cwd = Rye.escape(@safe, 'cd', @current_working_directory)
+ if @rye_current_working_directory
+ cwd = Rye.escape(@rye_safe, 'cd', @rye_current_working_directory)
cmd_clean = [cwd, cmd_clean].join(' && ')
end
# ditto (same explanation as cwd)
- if @current_umask
- cwd = Rye.escape(@safe, 'umask', @current_umask)
+ if @rye_current_umask
+ cwd = Rye.escape(@rye_safe, 'umask', @rye_current_umask)
cmd_clean = [cwd, cmd_clean].join(' && ')
end
info "COMMAND: #{cmd_clean}"
debug "Executing: %s" % cmd_clean
- if @pre_command_hook.is_a?(Proc)
- @pre_command_hook.call(cmd, args, opts[:user])
+ if @rye_pre_command_hook.is_a?(Proc)
+ @rye_pre_command_hook.call(cmd, args, opts[:user])
end
## NOTE: Do not raise a CommandNotFound exception in this method.
# We want it to be possible to define methods to a single instance
# of Rye::Box. i.e. def rbox.rm()...
@@ -556,12 +610,12 @@
rap.add_stderr(stderr || '')
rap.add_exit_code(ecode)
rap.exit_signal = esignal
rap.cmd = cmd
- if @post_command_hook.is_a?(Proc)
- @post_command_hook.call(rap)
+ if @rye_post_command_hook.is_a?(Proc)
+ @rye_post_command_hook.call(rap)
else
# It seems a convention for various commands to return -1
# when something only mildly concerning happens. ls even
# returns -1 for apparently no reason sometimes. In any
# case, the real errors are the ones greater than zero
@@ -621,11 +675,11 @@
# #puts "got stdout: #{data}"
# #channel.send_data "something for stdin\n"
#end
end
- channel = @ssh.exec(command, &block)
+ channel = @rye_ssh.exec(command, &block)
channel.wait # block until we get a response
channel[:exit_code] = 0 if channel[:exit_code] == nil
channel[:exit_code] &&= channel[:exit_code].to_i
@@ -653,12 +707,12 @@
direction ||= ''
unless [:upload, :download].member?(direction.to_sym)
raise "Must be one of: upload, download"
end
- if @current_working_directory
- info "CWD (#{@current_working_directory})"
+ if @rye_current_working_directory
+ info "CWD (#{@rye_current_working_directory})"
end
files = [files].flatten.compact || []
# We allow a single file to be downloaded into a StringIO object
@@ -672,11 +726,11 @@
if direction == :upload && other.is_a?(StringIO)
raise "Cannot upload to a StringIO object"
end
- # Fail early. We check the
+ # Fail early. We check whether the StringIO object is available to read
files.each do |file|
if file.is_a?(StringIO)
raise "Cannot download a StringIO object" if direction == :download
raise "StringIO object not opened for reading" if file.closed_read?
# If a StringIO object is at end of file, SCP will hang. (TODO: SCP)
@@ -692,16 +746,16 @@
if files.size > 1 && !other.is_a?(StringIO)
debug "CREATING TARGET DIRECTORY: #{other}"
self.mkdir(:p, other) unless self.file_exists?(other)
end
- Net::SCP.start(@host, @opts[:user], @opts || {}) do |scp|
+ Net::SCP.start(@rye_host, @rye_opts[:user], @rye_opts || {}) do |scp|
transfers = []
files.each do |file|
debug file.to_s
transfers << scp.send(direction, file, other) do |ch, n, s, t|
pinfo "#{n}: #{s}/#{t}b\r" # update line: "file: sent/total"
- @info.flush if @info # make sure every line is printed
+ @rye_info.flush if @rye_info # make sure every line is printed
end
end
transfers.each { |t| t.wait } # Run file transfers in parallel
info $/
end