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
All successfully established connections end up here for reuse later.
Set to 10M by default, this can be adjusted to tell scutil when to write command output to output.
Public Class Methods
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
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
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
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
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
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
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
# File lib/scutil.rb, line 340 def method_missing(method, *args, &block) return if ((method == :download) || (method == :upload)) super end
# 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
# 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