module Net::SSH::CLI

Constants

OPTIONS
VERSION

Attributes

channel[RW]
logger[RW]
net_ssh[RW]
new_data[RW]
process_count[RW]
proxy[RW]
stdout[RW]

Public Class Methods

new(**opts) click to toggle source
# File lib/net/ssh/cli.rb, line 32
def initialize(**opts)
  options.merge!(opts)
  self.net_ssh = options.delete(:net_ssh)
  self.logger = options.delete(:logger) || Logger.new(STDOUT, level: Logger::WARN)
  self.process_count = 0
  @new_data = String.new
end
start(**opts) click to toggle source

Example net_ssh = Net::SSH.start(“localhost”) net_ssh_cli = Net::SSH::CLI.start(net_ssh: net_ssh) net_ssh_cli.cmd “cat /etc/passwd”

> “root:x:0:0:root:/root:/bin/bashn…”

# File lib/net/ssh/cli.rb, line 28
def self.start(**opts)
  Net::SSH::CLI::Session.new(**opts)
end

Public Instance Methods

close_channel() click to toggle source
# File lib/net/ssh/cli.rb, line 406
def close_channel
  net_ssh&.cleanup_channel(channel) if channel
  self.channel = nil
end
cmd(command, pre_read: true, rm_prompt: cmd_rm_prompt, rm_command: cmd_rm_command, prompt: current_prompt, minimum_duration: cmd_minimum_duration, **opts) click to toggle source

send a command and get the output as return value

  1. sends the given command to the ssh connection channel

  2. continues to process the ssh connection until the prompt is found in the stdout

  3. prepares the output using your callbacks

  4. returns the output of your command

Hint: 'read' first on purpose as a feature. once you cmd you ignore what happend before. otherwise use read|write directly.

this should avoid many horrible state issues where the prompt is not the last prompt
# File lib/net/ssh/cli.rb, line 267
def cmd(command, pre_read: true, rm_prompt: cmd_rm_prompt, rm_command: cmd_rm_command, prompt: current_prompt, minimum_duration: cmd_minimum_duration, **opts)
  opts = opts.clone.merge(pre_read: pre_read, rm_prompt: rm_prompt, rm_command: rm_command, prompt: prompt)
  if pre_read
    pre_read_data = read
    logger.debug { "#cmd ignoring pre-command output: #{pre_read_data.inspect}" } if pre_read_data.present?
  end
  before_cmd_procs.each { |_name, a_proc| instance_eval(&a_proc) }
  write_n command
  sleep(minimum_duration)
  output = read_till(**opts)
  rm_prompt!(output, **opts)
  rm_command!(output, command, **opts)
  after_cmd_procs.each { |_name, a_proc| instance_eval(&a_proc) }
  output
rescue Error::ReadTillTimeout => error
  raise Error::CMD, "#{error.message} after cmd #{command.inspect} was sent"
end
Also aliased as: command, exec
cmds(*commands, **opts) click to toggle source

Execute multiple cmds, see cmd

# File lib/net/ssh/cli.rb, line 288
def cmds(*commands, **opts)
  commands.flatten.map { |command| [command, cmd(command, **opts)] }
end
Also aliased as: commands
command(command, pre_read: true, rm_prompt: cmd_rm_prompt, rm_command: cmd_rm_command, prompt: current_prompt, minimum_duration: cmd_minimum_duration, **opts)
Alias for: cmd
commands(*commands, **opts)
Alias for: cmds
current_prompt() click to toggle source

fancy prompt|prompt handling methods

# File lib/net/ssh/cli.rb, line 157
def current_prompt
  with_prompts[-1] || default_prompt
end
detect_prompt(seconds: 3) click to toggle source

tries to detect the prompt sends a “n”, waits for a X seconds, and uses the last line as prompt this won't work reliable if the prompt changes during the session

# File lib/net/ssh/cli.rb, line 182
def detect_prompt(seconds: 3)
  write_n
  process(seconds)
  self.default_prompt = read[/\n?^.*\z/]
  raise Error::PromptDetection, "couldn't detect a prompt" unless default_prompt.present?

  default_prompt
end
dialog(command, prompt, **opts) click to toggle source
# File lib/net/ssh/cli.rb, line 255
def dialog(command, prompt, **opts)
  opts = opts.clone.merge(prompt: prompt)
  cmd(command, **opts)
end
exec(command, pre_read: true, rm_prompt: cmd_rm_prompt, rm_command: cmd_rm_command, prompt: current_prompt, minimum_duration: cmd_minimum_duration, **opts)
Alias for: cmd
host() click to toggle source
# File lib/net/ssh/cli.rb, line 337
def host
  @net_ssh&.host
end
Also aliased as: hostname, to_s
hostname()
Alias for: host
impact(command, **opts) click to toggle source

the same as cmd but it will only run the command if the option run_impact is set to true. this can be used for commands which you might not want to run in development|testing mode but in production cli.impact(“reboot”)

> “skip: reboot”

cli.run_impact = true cli.impact(“reboot”)

> “system is going to reboot NOW”

# File lib/net/ssh/cli.rb, line 328
def impact(command, **opts)
  run_impact? ? cmd(command, **opts) : "skip: #{command.inspect}"
end
impacts(*commands, **opts) click to toggle source

same as cmds but for impact instead of cmd

# File lib/net/ssh/cli.rb, line 333
def impacts(*commands, **opts)
  commands.flatten.map { |command| [command, impact(command, **opts)] }
end
on_stdout(new_data) click to toggle source
# File lib/net/ssh/cli.rb, line 124
def on_stdout(new_data)
  self.new_data = new_data
  before_on_stdout_procs.each { |_name, a_proc| instance_eval(&a_proc) }
  stdout << new_data
  after_on_stdout_procs.each { |_name, a_proc| instance_eval(&a_proc) }
  optimise_stdout_processing
  stdout
end
open_channel() click to toggle source
# File lib/net/ssh/cli.rb, line 380
def open_channel # cli_channel
  before_open_channel_procs.each { |_name, a_proc| instance_eval(&a_proc) }
  ::Timeout.timeout(open_channel_timeout, Error::OpenChannelTimeout) do
    net_ssh.open_channel do |new_channel|
      logger.debug 'channel is open'
      self.channel = new_channel
      new_channel.request_pty(terminal_options) do |_ch, success|
        raise Error::Pty, "#{host || ip} Failed to open ssh pty" unless success
      end
      new_channel.send_channel_request('shell') do |_ch, success|
        raise Error::RequestShell, 'Failed to open ssh shell' unless success
      end
      new_channel.on_data do |_ch, data|
        on_stdout(data)
      end
      # new_channel.on_extended_data do |_ch, type, data| end
      # new_channel.on_close do end
    end
    until channel do process end
  end
  logger.debug 'channel is ready, running callbacks now'
  after_open_channel_procs.each { |_name, a_proc| instance_eval(&a_proc) }
  process
  self
end
options() click to toggle source
# File lib/net/ssh/cli.rb, line 75
def options
  @options ||= begin
    opts = OPTIONS.clone
    opts.each do |key, value|
      opts[key] = value.clone if value.is_a?(Hash)
    end
    opts
  end
end
options!(**opts) click to toggle source

don't even think about nesting hashes here

# File lib/net/ssh/cli.rb, line 86
def options!(**opts)
  options.merge!(opts)
end
options=(opts) click to toggle source
# File lib/net/ssh/cli.rb, line 90
def options=(opts)
  @options = ActiveSupport::HashWithIndifferentAccess.new(opts)
end
process(time = process_time) click to toggle source

have a deep look at the source of Net::SSH session#process github.com/net-ssh/net-ssh/blob/dd13dd44d68b7fa82d4ca9a3bbe18e30c855f1d2/lib/net/ssh/connection/session.rb#L227 session#loop github.com/net-ssh/net-ssh/blob/dd13dd44d68b7fa82d4ca9a3bbe18e30c855f1d2/lib/net/ssh/connection/session.rb#L179 because the (cli) channel stays open, we always need to ensure that the ssh layer gets “processed” further. This can be done inside here automatically or outside in a separate event loop for the net_ssh connection.

# File lib/net/ssh/cli.rb, line 374
def process(time = process_time)
  background_processing? ? sleep(time) : net_ssh.process(time)
rescue IOError => error
  raise Error, error.message
end
prompt_in_stdout?() click to toggle source
# File lib/net/ssh/cli.rb, line 239
def prompt_in_stdout?
  case current_prompt
  when Regexp
    !!stdout[current_prompt]
  when String
    stdout.include?(current_prompt)
  else
    raise Net::SSH::CLI::Error, "prompt/current_prompt is not a String/Regex #{current_prompt.inspect}"
  end
end
read() click to toggle source
# File lib/net/ssh/cli.rb, line 147
def read
  process
  var = stdout!
  logger.debug { "#read: \n#{var}" }
  var
end
read_for(seconds:) click to toggle source
# File lib/net/ssh/cli.rb, line 250
def read_for(seconds:)
  process(seconds)
  read
end
read_till(prompt: current_prompt, timeout: read_till_timeout, hard_timeout: read_till_hard_timeout, hard_timeout_factor: read_till_hard_timeout_factor, **_opts) click to toggle source

continues to process the ssh connection till stdout matches the given prompt. might raise a timeout error if a soft/hard timeout is given be carefull when using the hard_timeout, this is using the dangerous Timeout.timeout this gets really slow on large outputs, since the prompt will be searched in the whole output. Use z in the regex if possible

Optional named arguments:

- prompt: expected to be a regex
- timeout: nil or a number
- hard_timeout: nil, true, or a number
- hard_timeout_factor: nil, true, or a number
-   when hard_timeout == true, this will set the hard_timeout as (read_till_hard_timeout_factor * read_till_timeout), defaults to 1.2 = +20%
# File lib/net/ssh/cli.rb, line 219
def read_till(prompt: current_prompt, timeout: read_till_timeout, hard_timeout: read_till_hard_timeout, hard_timeout_factor: read_till_hard_timeout_factor, **_opts)
  raise Error::UndefinedMatch, 'no prompt given or default_prompt defined' unless prompt
  hard_timeout = (read_till_hard_timeout_factor * timeout) if timeout and hard_timeout == true
  hard_timeout = nil if hard_timeout == true

  with_prompt(prompt) do
    ::Timeout.timeout(hard_timeout, Error::ReadTillTimeout, "#{current_prompt.inspect} didn't match on #{stdout.inspect} within #{hard_timeout}s") do
      soft_timeout = Time.now + timeout if timeout
      until prompt_in_stdout? do
        if timeout and soft_timeout < Time.now
          raise Error::ReadTillTimeout, "#{current_prompt.inspect} didn't match on #{stdout.inspect} within #{timeout}s"
        end
        process
        sleep 0.01 # don't race for CPU
      end
    end
  end
  read
end
rm_command!(output, command, **opts) click to toggle source
# File lib/net/ssh/cli.rb, line 293
def rm_command!(output, command, **opts)
  output[command + cmd_rm_command_tail] = '' if rm_command?(**opts) && output[command + cmd_rm_command_tail]
end
rm_prompt!(output, prompt: current_prompt, **opts) click to toggle source

removes the prompt from the given output prompt should contain a named match 'prompt' /(?<prompt>.something.)z/ for backwards compatibility it also tries to replace the first match of the prompt /(something)z/ it removes the whole match if no matches are given /somethingz/

# File lib/net/ssh/cli.rb, line 301
def rm_prompt!(output, prompt: current_prompt, **opts)
  if rm_prompt?(**opts)
    if output[prompt]
      case prompt
      when String then output[prompt] = ''
      when Regexp
        if prompt.names.include?("prompt")
          output[prompt, "prompt"] = ''
        else
          begin
            output[prompt, 1] = ''
          rescue IndexError
            output[prompt] = ''
          end
        end
      end
    end
  end
end
sleep(duration) click to toggle source

if sleep_procs are set, they will be called instead of Kernel.sleep great for async .sleep_procs = proc do |duration| async_reactor.sleep(duration) end

cli.sleep(1)

# File lib/net/ssh/cli.rb, line 348
def sleep(duration)
  if sleep_procs.any?
    sleep_procs.each { |_name, a_proc| instance_exec(duration, &a_proc) }
  else
    Kernel.sleep(duration)
  end
end
stdin(content = String.new) click to toggle source
# File lib/net/ssh/cli.rb, line 133
def stdin(content = String.new)
  logger.debug { "#write #{content.inspect}" }
  before_on_stdin_procs.each { |_name, a_proc| instance_eval(&a_proc) }
  channel.send_data content
  process
  after_on_stdin_procs.each { |_name, a_proc| instance_eval(&a_proc) }
  content
end
Also aliased as: write
stdout!() click to toggle source
# File lib/net/ssh/cli.rb, line 118
def stdout!
  var = stdout
  self.stdout = String.new
  var
end
to_s()
Alias for: host
with_named_prompt(name) { || ... } click to toggle source

run something with a different named prompt

named_prompts = /(?<prompt>nroot)z/

with_named_prompt(“root”) do

cmd("sudo -i")
cmd("cat /etc/passwd")

end cmd(“exit”)

# File lib/net/ssh/cli.rb, line 171
def with_named_prompt(name)
  raise Error::UndefinedMatch, "unknown named_prompt #{name}" unless named_prompts[name]

  with_prompt(named_prompts[name]) do
    yield
  end
end
with_prompt(prompt) { || ... } click to toggle source

run something with a different prompt

with_prompt(/(?<prompt>nroot)z/) do

cmd("sudo -i")
cmd("cat /etc/passwd")

end cmd(“exit”)

# File lib/net/ssh/cli.rb, line 198
def with_prompt(prompt)
  logger.debug { "#with_prompt: #{current_prompt.inspect} => #{prompt.inspect}" }
  with_prompts << prompt
  yield
  prompt
ensure
  with_prompts.delete_at(-1)
  logger.debug { "#with_prompt: => #{current_prompt.inspect}" }
end
write(content = String.new)
Alias for: stdin
write_n(content = String.new) click to toggle source
# File lib/net/ssh/cli.rb, line 143
def write_n(content = String.new)
  write content + "\n"
end

Private Instance Methods

formatted_net_ssh_options() click to toggle source
# File lib/net/ssh/cli.rb, line 417
def formatted_net_ssh_options
  net_ssh_options.symbolize_keys.reject {|k,v| [:host, :ip, :user].include?(k)}
end
optimise_stdout_processing() click to toggle source

when new data is beeing received, likely more data will arrive - this improves the performance by a large factor but on a lot of data, this leads to a stack level too deep therefore it is limited to max on_stdout_processing the bigger on_stdout_processing, the closer we get to a stack level too deep

# File lib/net/ssh/cli.rb, line 433
def optimise_stdout_processing
  self.process_count += 1
  process unless process_count > on_stdout_processing
ensure
  self.process_count -= 1
end
rm_command?(**opts) click to toggle source
# File lib/net/ssh/cli.rb, line 425
def rm_command?(**opts)
  opts[:rm_cmd].nil? ? cmd_rm_command : opts[:rm_cmd]
end
rm_prompt?(**opts) click to toggle source
# File lib/net/ssh/cli.rb, line 421
def rm_prompt?(**opts)
  opts[:rm_prompt].nil? ? cmd_rm_prompt : opts[:rm_prompt]
end
terminal_options() click to toggle source
# File lib/net/ssh/cli.rb, line 440
def terminal_options
  {
    term: terminal_term,
    chars_wide: terminal_chars_width,
    chars_high: terminal_chars_height,
    pixels_wide: terminal_pixels_width,
    pixels_high: terminal_pixels_height,
    modes: terminal_modes
  }.reject {|k,v| v.nil?}
end
with_prompts() click to toggle source
# File lib/net/ssh/cli.rb, line 413
def with_prompts
  @with_prompts ||= []
end