lib/rye/box.rb in rye-0.3.2 vs lib/rye/box.rb in rye-0.4

- old
+ new

@@ -31,11 +31,13 @@ attr_accessor :host attr_accessor :safe attr_accessor :opts - + # The most recent value from Box.cd or Box.[] + attr_reader :current_working_directory + # * +host+ The hostname to connect to. The default is localhost. # * +opts+ a hash of optional arguments. # # The +opts+ hash excepts the following keys: # @@ -90,20 +92,21 @@ Rye::Cmd.instance_methods end alias :commands :can alias :cmds :can - + + # 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 + # 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 + # 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. # @@ -115,11 +118,10 @@ @current_working_directory = key self end alias :cd :'[]' - # 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. def connect raise Rye::NoHost unless @host @@ -161,151 +163,165 @@ # See Rye.keys def keys Rye.keys end + # Returns +@host+ + def to_s + @host + end - # Takes a command with arguments and returns it in a - # single String with escaped args and some other stuff. - # - # * +cmd+ The shell command name or absolute path. - # * +args+ an Array of command arguments. - # - # The command is searched for in the local PATH (where - # Rye is running). An exception is raised if it's not - # found. NOTE: Because this happens locally, you won't - # want to use this method if the environment is quite - # different from the remote machine it will be executed - # on. - # - # The command arguments are passed through Escape.shell_command - # (that means you can't use environment variables or asterisks). - # - def Box.prepare_command(cmd, *args) - args &&= [args].flatten.compact - cmd = Rye::Box.which(cmd) - raise CommandNotFound.new(cmd || 'nil') unless cmd - Rye::Box.escape(@safe, cmd, *args) + 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 - # An all ruby implementation of unix "which" command. - # - # * +executable+ the name of the executable - # - # Returns the absolute path if found in PATH otherwise nil. - def Box.which(executable) - return unless executable.is_a?(String) - #return executable if File.exists?(executable) # SHOULD WORK, MUST TEST - shortname = File.basename(executable) - dir = Rye.sysinfo.paths.select do |path| # dir contains all of the - next unless File.exists? path # occurrences of shortname - Dir.new(path).entries.member?(shortname) # found in the paths. - end - File.join(dir.first, shortname) unless dir.empty? # Return just the first + # Compares itself with the +other+ box. If the hostnames + # are the same, this will return true. Otherwise false. + def ==(other) + @host == other.host end - # Execute a local system command (via the shell, not SSH) - # - # * +cmd+ the executable path (relative or absolute) - # * +args+ Array of arguments to be sent to the command. Each element - # is one argument:. i.e. <tt>['-l', 'some/path']</tt> + # Returns the host SSH keys for this box + def host_key + raise "No host" unless @host + Rye.remote_host_keys(@host) + end + + # 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: shell is a bit paranoid so it escapes every argument. This means - # you can only use literal values. That means no asterisks too. + # NOTE: authorize_keys disables safe-mode for this box while it runs. # - def Box.shell(cmd, args=[]) - # TODO: allow stdin to be send to cmd - cmd = Box.prepare_command(cmd, args) - cmd << " 2>&1" # Redirect STDERR to STDOUT. Works in DOS also. - handle = IO.popen(cmd, "r") - output = handle.read.chomp - handle.close - output + def authorize_keys + added_keys = [] + opts[: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', '~/.ssh') # Silently create dir if it doesn't exist + self.echo("'#{k}' >> ~/.ssh/authorized_keys") + self.echo("'#{k}' >> ~/.ssh/authorized_keys2") + self.chmod('-R', '0600', '.ssh') + added_keys << path + end + opts[:safe] = true + added_keys end - # Creates a string from +cmd+ and +args+. If +safe+ is true - # it will send them through Escape.shell_command otherwise - # it will return them joined by a space character. - def Box.escape(safe, cmd, *args) - args = args.flatten.compact || [] - safe ? Escape.shell_command(cmd, *args).to_s : [cmd, args].flatten.compact.join(' ') + private + + + def debug(msg); @debug.puts msg if @debug; end + def error(msg); @error.puts msg if @error; end + + + # Add the current environment variables to the beginning of +cmd+ + def prepend_env(cmd) + return cmd unless @current_environment_variables.is_a?(Hash) + env = '' + @current_environment_variables.each_pair do |n,v| + env << "export #{n}=#{Escape.shell_single_word(v)}; " + end + [env, cmd].join(' ') end - private - - - def debug(msg); @debug.puts msg if @debug; end - def error(msg); @error.puts msg if @error; end + # Execute a command over SSH + # + # * +args+ is a command name and list of arguments. + # The command name is the literal name of the command + # that will be executed in the remote shell. The arguments + # will be thoroughly escaped and passed to the command. + # + # rbox = Rye::Box.new + # rbox.ls '-l', 'arg1', 'arg2' + # + # is equivalent to + # + # $ ls -l 'arg1' 'arg2' + # + # This method will try to connect to the host automatically + # but if it fails it will raise a Rye::NotConnected exception. + # + def run_command(*args) + connect if !@ssh || @ssh.closed? + args = args.flatten.compact + args = args.first.split(/\s+/) if args.size == 1 + cmd = args.shift - # Add the current environment variables to the beginning of +cmd+ - def prepend_env(cmd) - return cmd unless @current_environment_variables.is_a?(Hash) - env = '' - @current_environment_variables.each_pair do |n,v| - env << "export #{n}=#{Escape.shell_single_word(v)}; " - end - [env, cmd].join(' ') + # Symbols to switches. :l -> -l, :help -> --help + args.collect! do |a| + a = "-#{a}" if a.is_a?(Symbol) && a.to_s.size == 1 + a = "--#{a}" if a.is_a?(Symbol) + a end + raise Rye::NotConnected, @host unless @ssh && !@ssh.closed? - # Execute a command over SSH - # - # * +args+ is a command name and list of arguments. - # The command name is the literal name of the command - # that will be executed in the remote shell. The arguments - # will be thoroughly escaped and passed to the command. - # - # rbox = Rye::Box.new - # rbox.ls '-l', 'arg1', 'arg2' - # - # is equivalent to - # - # $ ls -l 'arg1' 'arg2' - # - # This method will try to connect to the host automatically - # but if it fails it will raise a Rye::NotConnected exception. - # - def add_command(*args) - connect if !@ssh || @ssh.closed? - args = args.first.split(/\s+/) if args.size == 1 - cmd, args = args.flatten.compact - - raise Rye::NotConnected, @host unless @ssh && !@ssh.closed? - - cmd_clean = Rye::Box.escape(@safe, cmd, args) - cmd_clean = prepend_env(cmd_clean) - if @current_working_directory - cwd = Rye::Box.escape(@safe, 'cd', @current_working_directory) - cmd_clean = [cwd, cmd_clean].join('; ') - end - debug "Executing: %s" % cmd_clean - stdout, stderr = net_ssh_exec! cmd_clean - rap = Rye::Rap.new(self) - rap.add_stdout(stdout || '') - rap.add_stderr(stderr || '') - rap + cmd_clean = Rye.escape(@safe, cmd, args) + cmd_clean = prepend_env(cmd_clean) + if @current_working_directory + cwd = Rye.escape(@safe, 'cd', @current_working_directory) + cmd_clean = [cwd, cmd_clean].join(' && ') end - alias :cmd :add_command - # Executes +command+ via SSH - # Returns an Array with two elements, [stdout, stderr], representing - # the STDOUT and STDERR output by the command. They're Strings. - def net_ssh_exec!(command) - block ||= Proc.new do |ch, type, data| - ch[:stdout] ||= "" - ch[:stderr] ||= "" - ch[:stdout] << data if type == :stdout - ch[:stderr] << data if type == :stderr + debug "Executing: %s" % cmd_clean + stdout, stderr, ecode, esignal = net_ssh_exec! cmd_clean + rap = Rye::Rap.new(self) + rap.add_stdout(stdout || '') + rap.add_stderr(stderr || '') + rap.exit_code = ecode + rap.exit_signal = esignal + rap.cmd = cmd + + raise Rye::CommandError.new(rap) if ecode > 0 + + rap + end + alias :cmd :run_command + + # Executes +command+ via SSH + # Returns an Array with 4 elements: [stdout, stderr, exit code, exit signal] + def net_ssh_exec!(command) + block ||= Proc.new do |channel, type, data| + channel[:stdout] ||= "" + channel[:stderr] ||= "" + channel[:stdout] << data if type == :stdout + channel[:stderr] << data if type == :stderr + channel.on_request("exit-status") do |ch, data| + # Anything greater than 0 is an error + channel[:exit_code] = data.read_long end - - channel = @ssh.exec(command, &block) - channel.wait # block until we get a response - - [channel[:stdout], channel[:stderr]] + 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 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 end + channel = @ssh.exec(command, &block) + channel.wait # block until we get a response + channel[:exit_code] ||= 0 + channel[:exit_code] &&= channel[:exit_code].to_i + + channel[:stderr].gsub!(/bash: line \d+:\s+/, '') if channel[:stderr] + + [channel[:stdout], channel[:stderr], channel[:exit_code], channel[:exit_signal]] + end + + end end