module Net::SSH::CLI
Constants
- OPTIONS
- VERSION
Attributes
Public Class Methods
# 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
Public Instance Methods
# File lib/net/ssh/cli.rb, line 406 def close_channel net_ssh&.cleanup_channel(channel) if channel self.channel = nil end
send a command and get the output as return value
-
sends the given command to the ssh connection channel
-
continues to process the ssh connection until the prompt is found in the stdout
-
prepares the output using your callbacks
-
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
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
fancy prompt|prompt handling methods
# File lib/net/ssh/cli.rb, line 157 def current_prompt with_prompts[-1] || default_prompt end
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
# File lib/net/ssh/cli.rb, line 255 def dialog(command, prompt, **opts) opts = opts.clone.merge(prompt: prompt) cmd(command, **opts) end
# File lib/net/ssh/cli.rb, line 337 def host @net_ssh&.host end
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
# 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
# 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
# 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
don't even think about nesting hashes here
# File lib/net/ssh/cli.rb, line 86 def options!(**opts) options.merge!(opts) end
# File lib/net/ssh/cli.rb, line 90 def options=(opts) @options = ActiveSupport::HashWithIndifferentAccess.new(opts) end
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
# 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
# File lib/net/ssh/cli.rb, line 147 def read process var = stdout! logger.debug { "#read: \n#{var}" } var end
# File lib/net/ssh/cli.rb, line 250 def read_for(seconds:) process(seconds) read end
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
# 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
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
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
# 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
# File lib/net/ssh/cli.rb, line 118 def stdout! var = stdout self.stdout = String.new var end
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
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
# File lib/net/ssh/cli.rb, line 143 def write_n(content = String.new) write content + "\n" end
Private Instance Methods
# 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
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
# File lib/net/ssh/cli.rb, line 425 def rm_command?(**opts) opts[:rm_cmd].nil? ? cmd_rm_command : opts[:rm_cmd] end
# File lib/net/ssh/cli.rb, line 421 def rm_prompt?(**opts) opts[:rm_prompt].nil? ? cmd_rm_prompt : opts[:rm_prompt] end
# 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
# File lib/net/ssh/cli.rb, line 413 def with_prompts @with_prompts ||= [] end