module Scutil

Constants

DEFAULT_OUTPUT_BUFFER_SIZE

By default, buffer 10M of data before writing.

DEFAULT_PTY_REGEX

Checks for a command starting with sudo by default.

DEFAULT_SUDO_PASSWD_FAILED_REGEX

Default password failed prompt is Sorry, try again.

DEFAULT_SUDO_PASSWD_REGEX

Default password prompt is [sudo] password for. Redhat based systems use Password: instead.

SCUTIL_VERSION

Attributes

connection_cache[RW]

All successfully established connections end up here for reuse later.

output_buffer_size[RW]

Set to 10M by default, this can be adjusted to tell scutil when to write command output to output.

Public Class Methods

check_pty_needed?(cmd, options, hostname) click to toggle source

Should we request a PTY? Uses custom regex if defined in :scutil_pty_regex.

# File lib/scutil.rb, line 60
def check_pty_needed?(cmd, options, hostname)      
  if options[:scutil_force_pty]
    return true
  end
  
  if !options[:scutil_pty_regex].kind_of? Regexp
    raise Scutil::Error.new(":scutil_pty_regex must be a kind of Regexp", hostname)
  end
  return (cmd =~ options[:scutil_pty_regex]) ? true : false
end
clear!(hostname) click to toggle source

Drops all instances of hostname from @connection_cache.

# File lib/scutil.rb, line 72
def clear!(hostname)
  if (Scutil.connection_cache.exists?(hostname))
    Scutil.connection_cache.remove(hostname)
  end
end
download(hostname, username, remote, local=nil, new_options={}, &progress) click to toggle source

Convenience method for downloading files. Only available if you have Net::SCP. This function simply calls Net::SCP#download! but reuses the SSH connection if it’s available. All options and semantics are identical to Scutil.exec_command and Net::SCP. If local is nil the downloaded file will be stored in a string in memory returned by download.

NB: This function currently calls the blocking Net::SCP#download! function rather than the non-blocking #download function. This is by design and will most likely be changed in the near future.

# File lib/scutil.rb, line 274
def download(hostname, username, remote, local=nil, new_options={}, &progress)
  options = get_default_options
  options.merge! new_options
  conn = find_connection(hostname, username, false, options)
  conn.scp.download!(remote, local, options, &progress)
end
exec_command(hostname, username, cmd, output=nil, new_options={}) click to toggle source

Scutil.exec_command is used to execute a command, specified in cmd, on a remote system. The return value and any ouput of the command are captured.

If output is a string it will be treated as a filename to be opened (mode ‘w’) and all command output will be written to this file. If output is an IO object it will be treated as an open file handle. Finally, if output is omitted, or an empty string, all command output will be directed to $stdout.

NB: This isn’t actually true. The only check made is to see if output responds to :write. The idea being that not only will a file handle have a write method, but also something like StringIO. Using StringIO here makes it easy to capture the command’s output in a string. Suggestions on a better way to do this are definitely welcome.

Scutil will automatically request a PTY if sudo is at the start of cmd. This is driven by a regex which is customizable via the option :scutil_pty_regex. You can also force a PTY request by specifying :scutil_force_pty in options.

Scutil.exec_command takes the following options:

  • :scutil_verbose => Extra output.

  • :scutil_force_pty => Force a PTY request (or not) for every channel.

  • :scutil_pty_regex => Specific a custom regex here for use when scutil decides whether or not to request a PTY.

  • :scutil_sudo_passwd_regex => If sudo requires a password you can specify the prompt to look for, e.g., Password: .

  • :scutil_sudo_passwd_failed_regex => Regular expression for a sudo password failure.

  • :scutil_sudo_passwd => The sudo password.

  • :scutil_suppress_stderr_exception => Prevent exception if we get data on stderr.

In addition, any other options passed Scutil.exec_command will be passed on to Net::SSH, except those prefixed with scutil_.

All calls to Scutil.exec_command, regardless of the way it’s used, will return the remote command’s return value:

retval = Scutil.exec_command('hostname', 'username', '/bin/true')
puts "True is false!" if retval != 0

See the test directory for more usage examples.

# File lib/scutil.rb, line 119
    def exec_command(hostname, username, cmd, output=nil, new_options={})
      # Fill in defaults
      options = get_default_options
      options.merge! new_options
      
      # Do we need a PTY?
      pty_needed = check_pty_needed? cmd, options, hostname
      
      conn = find_connection(hostname, username, pty_needed, options)
      
      fh = $stdout
      if (output.nil?)
        fh = $stdout
      elsif (output.respond_to? :write)  # XXX: This may not be a safe assumuption...
        fh = output
      elsif (output.class == String)
        fh = File.open(output, 'wb') unless output.empty?
      else
        raise Scutil::Error.new("Invalid output object type: #{output.class}.", hostname)
      end
      
      # If a custom password prompt regex has been defined, use it.
      passwd_regex = set_sudo_password_prompt(options)
      
      # If a custom bad password prompt regex has been defined, use it.
      passwd_failed_regex = set_sudo_password_failed(options)
      
      # Setup channel callbacks
      odata = ""
      edata = ""
      exit_status = 0
      # Catch the first call to on_data
      sudo_passwd_state = :new
      chan = conn.open_channel do |channel|
        print "[#{conn.host}:#{channel.local_id}] Setting up callbacks...\n" if options[:scutil_verbose]
        if (pty_needed)
          print "[#{conn.host}:#{channel.local_id}] Requesting PTY...\n" if options[:scutil_verbose]
          # OPOST is necessary, CS8 makes sense.  Revisit after broader testing.
          channel.request_pty(:modes => { Net::SSH::Connection::Term::CS8 => 1, 
                                Net::SSH::Connection::Term::OPOST => 0 } ) do |ch, success|
            raise Scutil::Error.new("Failed to get a PTY", hostname) if !success
          end
        end
        
        channel.on_data do |ch, data|
#          print "on_data: #{data.size}\n" if options[:scutil_verbose]
          
          # sudo password states are as follows:
          #  :new     => Connection established, first data packet.
          #  :waiting => Password sent, wating for reply.
          #  :done    => Authenication complete or not required.
          case (sudo_passwd_state)
          when :done
            odata += data
          when :new
            if (data =~ passwd_regex) # We have been prompted for a sudo password
              if (options[:scutil_sudo_passwd].nil?) # No password defined, bail
                raise Scutil::Error.new("[#{conn.host}:#{channel.local_id}] Password required for sudo.  
Define in :scutil_sudo_passwd.", hostname)
                channel.close
              end
              ch.send_data options[:scutil_sudo_passwd] + "\n"
              sudo_passwd_state = :waiting
            else # No sudo password needed, grab the data and move on.
              odata += data
              sudo_passwd_state = :done
            end
          when :waiting
            if (data =~ passwd_failed_regex) # Bad sudo password
              raise Scutil::Error.new("[#{conn.host}:#{channel.local_id}] Password failed for sudo.  
Define in :scutil_sudo_passwd or check :scutil_sudo_failed_passwd for the correct failure response.", 
                                      hostname)
              channel.close
              sudo_passwd_state = :done
            else
              # NoOp for "\n"
            end
          else
            raise Scutil::Error.new("[#{conn.host}:#{channel.local_id}] Invalid connection state.", hostname)
          end
          
          # Only buffer some of the output before writing to disk (10M by default).
          if (odata.size >= Scutil.output_buffer_size)
            fh.write odata
            odata = ""
          end
        end
        
        channel.on_extended_data do |ch, type, data|
          print "on_extended_data: #{data.size}\n" if options[:scutil_verbose]
          edata += data
        end
        
        channel.on_close do |ch|
          print "[#{conn.host}:#{channel.local_id}] on_close\n" if options[:scutil_verbose]
        end
        
        channel.on_open_failed do |ch, code, desc|
          raise Scutil::Error.new("Failed to open channel: #{desc}", hostname, code) if !success
        end
        
        channel.on_request("exit-status") do |ch, data|
          exit_status = data.read_long
          print "[#{conn.host}:#{channel.local_id}] on_request(\"exit-status\"): #{exit_status}\n" if options[:scutil_verbose]
        end
        
        channel.exec(cmd)
      end
      
      conn.loop
      
      # Write whatever is left
      fh.write odata
      
      # Close the file or you'll chase red herrings for two hours...
      fh.close unless fh == $stdout
      
      # If extended_data was recieved there was a problem...
      raise Scutil::Error.new("Error: #{edata}", hostname, exit_status) unless (edata.empty?) || options[:scutil_suppress_stderr_exception]
      
      # The return value of the remote command.
      return exit_status
    end
find_connection(hostname, username, pty_needed, options) click to toggle source

Check for an existing connection in the cache based on hostname. If the hostname exists find a suitable connection. Otherwise establish a connection and add it to the pool.

# File lib/scutil.rb, line 285
def find_connection(hostname, username, pty_needed, options)
  conn = nil
  begin
    if (Scutil.connection_cache.exists?(hostname))
      sys_conn = Scutil.connection_cache.fetch(hostname)
      print "[#{hostname}] Using existing connection\n" if options[:scutil_verbose]
      conn = sys_conn.get_connection(hostname, username, pty_needed, options)
    else
      sys_conn = SystemConnection.new(hostname, options)
      # Call get_connection first.  Don't add to cache unless established.
      conn = sys_conn.get_connection(hostname, username, pty_needed, options)
      print "[#{hostname}] Adding new connection to cache\n" if options[:scutil_verbose]
      Scutil.connection_cache << sys_conn
    end
  rescue Net::SSH::AuthenticationFailed => err
    raise Scutil::Error.new("Authenication failed for user: #{username}", hostname)
  rescue SocketError => err
    raise Scutil::Error.new(err.message, hostname)
  rescue Errno::ETIMEDOUT => err
    raise Scutil::Error.new(err.message, hostname)
  end
  return conn
end
upload(hostname, username, local, remote, new_options={}, &progress) click to toggle source

Convenience method for uploading files. Only available if you have Net::SCP. This function simply calls Net::SCP#upload! but reuses the SSH connection if it’s available. All options and semantics are identical to Scutil.exec_command and Net::SCP.

NB: This function currently calls the blocking Net::SCP#upload! function rather than the non-blocking #upload function. This is by design and will most likely be changed in the near future.

# File lib/scutil.rb, line 257
def upload(hostname, username, local, remote, new_options={}, &progress)
  options = get_default_options
  options.merge! new_options
  conn = find_connection(hostname, username, false, options)
  conn.scp.upload!(local, remote, options, &progress)
end

Private Class Methods

get_default_options() click to toggle source

Set the default options for connection.

# File lib/scutil.rb, line 311
def get_default_options
  { 
    :scutil_verbose                  => false,
    :scutil_force_pty                => false,
    :scutil_pty_regex                => DEFAULT_PTY_REGEX,
    :scutil_sudo_passwd_regex        => DEFAULT_SUDO_PASSWD_REGEX,
    :scutil_sudo_passwd_failed_regex => DEFAULT_SUDO_PASSWD_FAILED_REGEX,
    :scutil_sudo_passwd              => nil
  }
end
method_missing(method, *args, &block) click to toggle source
Calls superclass method
# File lib/scutil.rb, line 340
def method_missing(method, *args, &block)
  return if ((method == :download) || (method == :upload))
  super
end
set_sudo_password_failed(options) click to toggle source
# File lib/scutil.rb, line 331
def set_sudo_password_failed(options)
  if (!options[:scutil_sudo_passwd_failed_regex].nil? && 
      (options[:scutil_sudo_passwd_failed_regex].kind_of? Regexp))
    return options[:scutil_sudo_passwd_failed_regex]
  else
    raise Scutil::Error.new(":scutil_sudo_passwd_failed_regex must be a kind of Regexp", hostname)
  end
end
set_sudo_password_prompt(options) click to toggle source
# File lib/scutil.rb, line 322
def set_sudo_password_prompt(options)
  if (!options[:scutil_sudo_passwd_regex].nil? && 
      (options[:scutil_sudo_passwd_regex].kind_of? Regexp))
    return options[:scutil_sudo_passwd_regex]
  else
    raise Scutil::Error.new(":scutil_sudo_passwd_regex must be a kind of Regexp", hostname)
  end
end