lib/ztk/ssh.rb in ztk-0.0.5 vs lib/ztk/ssh.rb in ztk-0.0.6

- old
+ new

@@ -21,182 +21,314 @@ require "ostruct" require "net/ssh" require "net/ssh/proxy/command" require "net/sftp" -################################################################################ - module ZTK -################################################################################ - + # ZTK::SSH error class class SSHError < Error; end -################################################################################ + # We can get a new instance of SSH like so: + # ssh = ZTK::SSH.new + # + # If we wanted to redirect STDOUT and STDERR to a StringIO we can do this: + # std_combo = StringIO.new + # ssh = ZTK::SSH.new(:stdout => std_combo, :stderr => std_combo) + # + # If you want to specify SSH options you can: + # keys = File.expand_path(File.join(ENV['HOME'], '.ssh', 'id_rsa')) + # ssh = ZTK::SSH.new(:host_name => '127.0.0.1', :user => ENV['USER'], :keys => keys) + # + # = Configuration Examples: + # + # To proxy through another host, for example SSH to 192.168.1.1 through 192.168.0.1: + # ssh.config do |config| + # config.user = ENV['USER'] + # config.host_name = '192.168.1.1' + # config.proxy_user = ENV['USER'] + # config.proxy_host_name = '192.168.0.1' + # end + # + # Specify an identity file: + # ssh.config do |config| + # config.keys = File.expand_path(File.join(ENV['HOME'], '.ssh', 'id_rsa')) + # config.proxy_keys = File.expand_path(File.join(ENV['HOME'], '.ssh', 'id_rsa')) + # end + # + # Specify a timeout: + # ssh.config do |config| + # config.timeout = 30 + # end + # + # Specify a password: + # ssh.config do |config| + # config.password = 'p@$$w0rd' + # end + # + # Check host keys, the default is false (off): + # ssh.config do |config| + # config.host_key_verify = true + # end + class SSH < ZTK::Base - class SSH < ::ZTK::Base - -################################################################################ - + # @param [Hash] config Configuration options hash. + # @option config [String] :host_name Server hostname to connect to. + # @option config [String] :user Username to use for authentication. + # @option config [String, Array<String>] :keys A single or series of identity files to use for authentication. + # @option config [String] :password Password to use for authentication. + # @option config [Integer] :timeout SSH connection timeout to use. + # @option config [Boolean] :compression Weither or not to use compression for this session. + # @option config [Integer] :compression_level What level of compression to use. + # @option config [String] :proxy_host_name Server hostname to proxy through. + # @option config [String] :proxy_user Username to use for proxy authentication. + # @option config [String, Array<String>] :proxy_keys A single or series of identity files to use for authentication with the proxy. def initialize(config={}) - super({ - :ssh => ::OpenStruct.new - }.merge(config)) + super(config) end -################################################################################ - + # Launches an SSH console, replacing the current process with the console process. + # + # @example Launch a console: + # $logger = ZTK::Logger.new(STDOUT) + # ssh = ZTK::SSH.new + # ssh.config do |config| + # config.user = ENV["USER"] + # config.host_name = "127.0.0.1" + # end + # ssh.console def console - @config.logger and @config.logger.debug { "config(#{@config.ssh.inspect})" } + log(:debug) { "console" } + log(:debug) { "config(#{@config.inspect})" } - command = [ "ssh" ] - command << [ "-q" ] - command << [ "-o", "UserKnownHostsFile=/dev/null" ] - command << [ "-o", "StrictHostKeyChecking=no" ] - command << [ "-o", "KeepAlive=yes" ] - command << [ "-o", "ServerAliveInterval=60" ] - command << [ "-i", @config.ssh.identity_file ] if @config.ssh.identity_file - command << [ "-o", "ProxyCommand=\"#{proxy_command}\"" ] if @config.ssh.proxy - command << "#{@config.ssh.user}@#{@config.ssh.host}" - command = command.flatten.compact.join(" ") - @config.logger and @config.logger.info { "command(#{command})" } - ::Kernel.exec(command) + Kernel.exec(console_command) end -################################################################################ - + # Executes a command on the remote host. + # + # @param [String] command The command to execute. + # @param [Hash] options The options hash for executing the command. + # @option options [Boolean] :silence Squelch output to STDOUT and STDERR. If the log level is :debug, STDOUT and STDERR will go to the log file regardless of this setting. STDOUT and STDERR are always returned in the output return value regardless of this setting. + # + # @return [OpenStruct#output] The output of the command, both STDOUT and STDERR. + # @return [OpenStruct#exit] The exit status (i.e. $?). + # + # @example Execute a command: + # $logger = ZTK::Logger.new(STDOUT) + # ssh = ZTK::SSH.new + # ssh.config do |config| + # config.user = ENV["USER"] + # config.host_name = "127.0.0.1" + # end + # puts ssh.exec("hostname -f").output def exec(command, options={}) - @ssh ||= ::Net::SSH.start(@config.ssh.host, @config.ssh.user, ssh_options) + log(:debug) { "exec(#{command.inspect}, #{options.inspect})" } + log(:debug) { "config(#{@config.inspect})" } - options = { :silence => false }.merge(options) - silence = options[:silence] - output = "" + @ssh ||= Net::SSH.start(@config.host_name, @config.user, ssh_options) + log(:debug) { "ssh(#{@ssh.inspect})" } - @config.logger and @config.logger.debug { "config(#{@config.ssh.inspect})" } - @config.logger and @config.logger.debug { "options(#{options.inspect})" } - @config.logger and @config.logger.info { "command(#{command})" } + options = OpenStruct.new({ :silence => false }.merge(options)) + log(:debug) { "options(#{options.inspect})" } + + output = "" channel = @ssh.open_channel do |chan| - @config.logger and @config.logger.debug { "channel opened" } + log(:debug) { "channel opened" } chan.exec(command) do |ch, success| raise SSHError, "Could not execute '#{command}'." unless success ch.on_data do |c, data| - output += data - @config.logger and @config.logger.debug { data.chomp.strip } - @config.stdout.print(data) if !silence + log(:debug) { data.chomp.strip } + @config.stdout.print(data) unless options.silence + output += data.chomp.strip end ch.on_extended_data do |c, type, data| - output += data - @config.logger and @config.logger.debug { data.chomp.strip } - @config.stderr.print(data) if !silence + log(:debug) { data.chomp.strip } + @config.stderr.print(data) unless options.silence + output += data.chomp.strip end end end channel.wait - @config.logger and @config.logger.debug { "channel closed" } + log(:debug) { "channel closed" } - output + OpenStruct.new(:output => output, :exit => $?) end -################################################################################ - + # Uploads a local file to a remote host. + # + # @param [String] local The local file/path you wish to upload from. + # @param [String] remote The remote file/path you with to upload to. + # + # @example Upload a file: + # $logger = ZTK::Logger.new(STDOUT) + # ssh = ZTK::SSH.new + # ssh.config do |config| + # config.user = ENV["USER"] + # config.host_name = "127.0.0.1" + # end + # local = File.expand_path(File.join(ENV["HOME"], ".ssh", "id_rsa.pub")) + # remote = File.expand_path(File.join("/tmp", "id_rsa.pub")) + # ssh.upload(local, remote) def upload(local, remote) - @sftp ||= ::Net::SFTP.start(@config.ssh.host, @config.ssh.user, ssh_options) + log(:debug) { "upload(#{local.inspect}, #{remote.inspect})" } + log(:debug) { "config(#{@config.inspect})" } - @config.logger and @config.logger.debug { "config(#{@config.ssh.inspect})" } - @config.logger and @config.logger.info { "parameters(#{local},#{remote})" } + @sftp ||= Net::SFTP.start(@config.host_name, @config.user, ssh_options) + log(:debug) { "sftp(#{@sftp.inspect})" } + @sftp.upload!(local.to_s, remote.to_s) do |event, uploader, *args| case event when :open - @config.logger and @config.logger.info { "upload(#{args[0].local} -> #{args[0].remote})" } + log(:info) { "upload(#{args[0].local} -> #{args[0].remote})" } when :close - @config.logger and @config.logger.debug { "close(#{args[0].remote})" } + log(:debug) { "close(#{args[0].remote})" } when :mkdir - @config.logger and @config.logger.debug { "mkdir(#{args[0]})" } + log(:debug) { "mkdir(#{args[0]})" } when :put - @config.logger and @config.logger.debug { "put(#{args[0].remote}, size #{args[2].size} bytes, offset #{args[1]})" } + log(:debug) { "put(#{args[0].remote}, size #{args[2].size} bytes, offset #{args[1]})" } when :finish - @config.logger and @config.logger.info { "finish" } + log(:info) { "finish" } end end true end -################################################################################ - + # Downloads a remote file to the local host. + # + # @param [String] remote The remote file/path you with to download from. + # @param [String] local The local file/path you wish to download to. + # + # @example Download a file: + # $logger = ZTK::Logger.new(STDOUT) + # ssh = ZTK::SSH.new + # ssh.config do |config| + # config.user = ENV["USER"] + # config.host_name = "127.0.0.1" + # end + # local = File.expand_path(File.join("/tmp", "id_rsa.pub")) + # remote = File.expand_path(File.join(ENV["HOME"], ".ssh", "id_rsa.pub")) + # ssh.download(remote, local) def download(remote, local) - @sftp ||= ::Net::SFTP.start(@config.ssh.host, @config.ssh.user, ssh_options) + log(:debug) { "download(#{remote.inspect}, #{local.inspect})" } + log(:debug) { "config(#{@config.inspect})" } - @config.logger and @config.logger.debug { "config(#{@config.ssh.inspect})" } - @config.logger and @config.logger.info { "parameters(#{remote},#{local})" } + @sftp ||= Net::SFTP.start(@config.host_name, @config.user, ssh_options) + log(:debug) { "sftp(#{@sftp.inspect})" } + @sftp.download!(remote.to_s, local.to_s) do |event, downloader, *args| case event when :open - @config.logger and @config.logger.info { "download(#{args[0].remote} -> #{args[0].local})" } + log(:info) { "download(#{args[0].remote} -> #{args[0].local})" } when :close - @config.logger and @config.logger.debug { "close(#{args[0].local})" } + log(:debug) { "close(#{args[0].local})" } when :mkdir - @config.logger and @config.logger.debug { "mkdir(#{args[0]})" } + log(:debug) { "mkdir(#{args[0]})" } when :get - @config.logger and @config.logger.debug { "get(#{args[0].remote}, size #{args[2].size} bytes, offset #{args[1]})" } + log(:debug) { "get(#{args[0].remote}, size #{args[2].size} bytes, offset #{args[1]})" } when :finish - @config.logger and @config.logger.info { "finish" } + log(:info) { "finish" } end end true end -################################################################################ private -################################################################################ + # Builds our SSH console command. + def console_command + log(:debug) { "console_command" } + log(:debug) { "config(#{@config.inspect})" } + + command = [ "ssh" ] + command << [ "-q" ] + command << [ "-A" ] + command << [ "-o", "UserKnownHostsFile=/dev/null" ] + command << [ "-o", "StrictHostKeyChecking=no" ] + command << [ "-o", "KeepAlive=yes" ] + command << [ "-o", "ServerAliveInterval=60" ] + command << [ "-i", @config.keys ] if @config.keys + command << [ "-o", "ProxyCommand=\"#{proxy_command}\"" ] if @config.proxy_host_name + command << "#{@config.user}@#{@config.host_name}" + command = command.flatten.compact.join(" ") + log(:debug) { "console_command(#{command.inspect})" } + command + end + + # Builds our SSH proxy command. def proxy_command - @config.logger and @config.logger.debug { "config(#{@config.ssh.inspect})" } + log(:debug) { "proxy_command" } + log(:debug) { "config(#{@config.inspect})" } - if !@config.ssh.identity_file - message = "You must specify an identity file in order to SSH proxy." - @config.logger and @config.logger.fatal { message } + if !@config.proxy_user + message = "You must specify an proxy user in order to SSH proxy." + log(:fatal) { message } raise SSHError, message end + if !@config.proxy_host_name + message = "You must specify an proxy host_name in order to SSH proxy." + log(:fatal) { message } + raise SSHError, message + end + command = ["ssh"] command << [ "-q" ] + command << [ "-A" ] command << [ "-o", "UserKnownHostsFile=/dev/null" ] command << [ "-o", "StrictHostKeyChecking=no" ] command << [ "-o", "KeepAlive=yes" ] command << [ "-o", "ServerAliveInterval=60" ] - command << [ "-i", @config.ssh.proxy_identity_file ] if @config.ssh.proxy_identity_file - command << "#{@config.ssh.proxy_user}@#{@config.ssh.proxy_host}" + command << [ "-i", @config.proxy_keys ] if @config.proxy_keys + command << "#{@config.proxy_user}@#{@config.proxy_host_name}" command << "nc %h %p" command = command.flatten.compact.join(" ") - @config.logger and @config.logger.debug { "command(#{command})" } + log(:debug) { "proxy_command(#{command.inspect})" } command end -################################################################################ - + # Builds our SSH options hash. def ssh_options - @config.logger and @config.logger.debug { "config(#{@config.ssh.inspect})" } - options = {} - options.merge!(:password => @config.ssh.password) if @config.ssh.password - options.merge!(:keys => @config.ssh.identity_file) if @config.ssh.identity_file - options.merge!(:timeout => @config.ssh.timeout) if @config.ssh.timeout - options.merge!(:user_known_hosts_file => '/dev/null') if !@config.ssh.host_key_verify - options.merge!(:proxy => ::Net::SSH::Proxy::Command.new(proxy_command)) if @config.ssh.proxy - @config.logger and @config.logger.debug { "options(#{options.inspect})" } + log(:debug) { "ssh_options" } + log(:debug) { "config(#{@config.inspect})" } + + options = { + :forward_agent => true, + :compression => false, + :user_known_hosts_file => '/dev/null' + } + + # These are plainly documented on the Net::SSH config class. + options.merge!(:encryption => @config.encryption) if @config.encryption + options.merge!(:compression => @config.compression) if @config.compression + options.merge!(:compression_level => @config.compression_level) if @config.compression_level + options.merge!(:timeout => @config.timeout) if @config.timeout + options.merge!(:forward_agent => @config.forward_agent) if @config.forward_agent + options.merge!(:global_known_hosts_file => @config.global_known_hosts_file) if @config.global_known_hosts_file + options.merge!(:auth_methods => @config.auth_methods) if @config.auth_methods + options.merge!(:host_key => @config.host_key) if @config.host_key + options.merge!(:host_key_alias => @config.host_key_alias) if @config.host_key_alias + options.merge!(:host_name => @config.host_name) if @config.host_name + options.merge!(:keys => @config.keys) if @config.keys + options.merge!(:keys_only => @config.keys_only) if @config.keys_only + options.merge!(:hmac => @config.hmac) if @config.hmac + options.merge!(:port => @config.port) if @config.port + options.merge!(:proxy => Net::SSH::Proxy::Command.new(proxy_command)) if @config.proxy_host_name + options.merge!(:rekey_limit => @config.rekey_limit) if @config.rekey_limit + options.merge!(:user => @config.user) if @config.user + options.merge!(:user_known_hosts_file => @config.user_known_hosts_file) if @config.user_known_hosts_file + + # This is not plainly documented on the Net::SSH config class. + options.merge!(:password => @config.password) if @config.password + + log(:debug) { "ssh_options(#{options.inspect})" } options end -################################################################################ - end -################################################################################ - end - -################################################################################