require 'tempfile' module Gofer # A persistent, authenticated SSH connection to a single host. # # Connections are persistent, but not encapsulated within a shell. # This means that while it won't need to reconnect & re-authenticate for # each operation, don't assume that environment variables will be # persisted between commands like they will in a shell-based SSH session. # # +/etc/ssh/config+ and ~/.ssh/config are not recognized by Net::SSH, and thus # not recognized by Gofer::Host. class Host attr_reader :hostname attr_accessor :quiet, :output_prefix # Create a new connection to a host # # Passed options not included in the below are passed directly to # Net::SSH.start. See http://net-ssh.github.com/ssh/v2/api/index.html # for valid arguments. # # Options: # # +quiet+:: Don't print stdout output from +run+ commands # +output_prefix+:: Prefix each line of stdout and stderr to differentiate multiple host output def initialize _hostname, username, opts={} @hostname = _hostname # support legacy positional argument use if opts.is_a? String warn "Gofer::Host.new identify file positional argument will be removed in 1.0, use :keys instead" opts = { :keys => [opts]} end @quiet = opts.delete(:quiet) @output_prefix = opts.delete(:output_prefix) # support legacy identity_file argument if opts[:identity_file] warn "Gofer::Host.new option :identify_file will be removed in 1.0, use :keys instead" opts[:keys] = [opts.delete(:identity_file)] end @ssh = SshWrapper.new(hostname, username, opts) end # Run +command+. # # Will raise an error if +command+ exits with a non-zero status, unless # +capture_exit_status+ is true. # # Print +stdout+ and +stderr+ as they're received. # # Returns an intance of Gofer::Response, containing captured +stdout+, # +stderr+, and an exit status if +capture_exit_status+ is true. # # Options: # # +quiet+:: Don't print +stdout+, can also be set with +quiet=+ on the instance # +quiet_stderr+:: Don't print +stderr+ # +capture_exit_status+:: Don't raise an error on a non-zero exit status def run command, opts={} opts[:quiet] = quiet unless opts.include?(:quiet) opts[:output_prefix] = @output_prefix response = @ssh.run command, opts if !opts[:capture_exit_status] && response.exit_status != 0 raise HostError.new(self, response, "Command #{command} failed with exit status #{@ssh.last_exit_status}") end response end # Returns +true+ if +path+ exists, +false+ otherwise. def exist? path @ssh.run("sh -c '[ -e #{path} ]'").exit_status == 0 end # Returnss the contents of the file at +path+. def read path @ssh.read_file path end # Returns +true+ if +path+ is a directory, +false+ otherwise. def directory? path @ssh.run("sh -c '[ -d #{path} ]'").exit_status == 0 end # Returns a list of the files in the directory at +path+. def ls path response = @ssh.run "ls -1 #{path}", :quiet => true if response.exit_status == 0 response.stdout.strip.split("\n") else raise HostError.new(self, response, "Could not list #{path}, exit status #{response.exit_status}") end end # Uploads the file or directory at +from+ to +to+. # # Options: # # +recursive+: Perform a recursive upload, similar to +scp -r+. +true+ by default if +from+ is a directory. def upload from, to, opts = {} @ssh.upload from, to, {:recursive => File.directory?(from)}.merge(opts) end # Downloads the file or directory at +from+ to +to+ # # Options: # # +recursive+: Perform a recursive download, similar to +scp -r+. +true+ by default if +from+ is a directory. def download from, to, opts = {} @ssh.download from, to, {:recursive => directory?(from)}.merge(opts) end # Writes +data+ to a file at +to+ def write data, to Tempfile.open "gofer_write" do |file| file.write data file.close @ssh.upload(file.path, to, :recursive => false) end end end end