Module: Net::SSH::CLI
- Included in:
- Session
- Defined in:
- lib/net/ssh/cli.rb,
lib/net/ssh/cli/version.rb
Defined Under Namespace
Constant Summary collapse
- OPTIONS =
ActiveSupport::HashWithIndifferentAccess.new( default_prompt: /\n?^(\S+@.*)\z/, # the default prompt to search for. It is recommended to use \z to ensure you don't match the prompt too early. cmd_rm_prompt: false, # whether the prompt should be removed in the output of #cmd cmd_rm_command: false, # whether the given command should be removed in the output of #cmd cmd_rm_command_tail: "\n", # which format does the end of line return after a command has been submitted. Could be something like "ls\n" "ls\r\n" or "ls \n" (extra spaces) cmd_minimum_duration: 0, # how long do you want to wait/sleep after sending the command. After this waiting time, the output will be processed and the prompt will be searched. run_impact: false, # whether to run #impact commands. This might align with testing|development|production. example #impact("reboot") read_till_timeout: nil, # timeout for #read_till to find the match read_till_hard_timeout: nil, # hard timeout for #read_till to find the match using Timeout.timeout(hard_timeout) {}. Might creates unpredicted sideffects read_till_hard_timeout_factor: 1.2, # hard timeout factor in case read_till_hard_timeout is true named_prompts: ActiveSupport::HashWithIndifferentAccess.new, # you can used named prompts for #with_prompt {} before_cmd_procs: ActiveSupport::HashWithIndifferentAccess.new, # procs to call before #cmd after_cmd_procs: ActiveSupport::HashWithIndifferentAccess.new, # procs to call after #cmd before_on_stdout_procs: ActiveSupport::HashWithIndifferentAccess.new, # procs to call before data arrives from the underlying connection after_on_stdout_procs: ActiveSupport::HashWithIndifferentAccess.new, # procs to call after data arrives from the underlying connection before_on_stdin_procs: ActiveSupport::HashWithIndifferentAccess.new, # procs to call before data is sent to the underlying channel after_on_stdin_procs: ActiveSupport::HashWithIndifferentAccess.new, # procs to call after data is sent to the underlying channel before_open_channel_procs: ActiveSupport::HashWithIndifferentAccess.new, # procs to call before opening a channel after_open_channel_procs: ActiveSupport::HashWithIndifferentAccess.new, # procs to call after opening a channel, for example you could call #detect_prompt or #read_till open_channel_timeout: nil, # timeout to open the channel net_ssh_options: ActiveSupport::HashWithIndifferentAccess.new, # a wrapper for options to pass to Net::SSH.start in case net_ssh is undefined process_time: 0.00001, # how long #process is processing net_ssh#process or sleeping (waiting for something) background_processing: false, # default false, whether the process method maps to the underlying net_ssh#process or the net_ssh#process happens in a separate loop on_stdout_processing: 100, # whether to optimize the on_stdout performance by calling #process #optimize_on_stdout-times in case more data arrives sleep_procs: ActiveSupport::HashWithIndifferentAccess.new, # procs to call instead of Kernel.sleep(), perfect for async hooks terminal_chars_width: 320, # Sets and sends the terminal dimensions during the opening of the channel. It does not send a channel_request on change. terminal_chars_height: 120, # See also https://github.com/net-ssh/net-ssh/blob/master/lib/net/ssh/connection/channel.rb#L220 section 'def request_pty' terminal_pixels_width: 1920, # See also https://www.ietf.org/rfc/rfc4254.txt section pty-req and section window-change terminal_pixels_height: 1080, # terminal_term: nil, # Sets the terminal term, usually xterm terminal_modes: nil, # )
- VERSION =
'1.9.1'
Instance Attribute Summary collapse
-
#channel ⇒ Object
Returns the value of attribute channel.
-
#logger ⇒ Object
Returns the value of attribute logger.
-
#net_ssh ⇒ Object
(also: #proxy)
NET::SSH.
-
#new_data ⇒ Object
Returns the value of attribute new_data.
-
#process_count ⇒ Object
Returns the value of attribute process_count.
-
#stdout ⇒ Object
Returns the value of attribute stdout.
Class Method Summary collapse
-
.start(**opts) ⇒ Object
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…”.
Instance Method Summary collapse
- #close_channel ⇒ Object
-
#cmd(command, pre_read: true, rm_prompt: cmd_rm_prompt, rm_command: cmd_rm_command, prompt: current_prompt, minimum_duration: cmd_minimum_duration, **opts) ⇒ Object
(also: #command, #exec)
send a command and get the output as return value 1.
-
#cmds(*commands, **opts) ⇒ Object
(also: #commands)
Execute multiple cmds, see #cmd.
-
#current_prompt ⇒ Object
fancy prompt|prompt handling methods.
-
#detect_prompt(seconds: 3) ⇒ Object
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.
- #dialog(command, prompt, **opts) ⇒ Object
- #host ⇒ Object (also: #hostname, #to_s)
-
#impact(command, **opts) ⇒ Object
the same as #cmd but it will only run the command if the option run_impact is set to true.
-
#impacts(*commands, **opts) ⇒ Object
same as #cmds but for #impact instead of #cmd.
- #initialize(**opts) ⇒ Object
- #on_stdout(new_data) ⇒ Object
-
#open_channel ⇒ Object
cli_channel.
- #options ⇒ Object
-
#options!(**opts) ⇒ Object
don’t even think about nesting hashes here.
- #options=(opts) ⇒ Object
-
#process(time = process_time) ⇒ Object
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.
- #prompt_in_stdout? ⇒ Boolean
- #read ⇒ Object
- #read_for(seconds:) ⇒ Object
-
#read_till(prompt: current_prompt, timeout: read_till_timeout, hard_timeout: read_till_hard_timeout, hard_timeout_factor: read_till_hard_timeout_factor, **_opts) ⇒ Object
continues to process the ssh connection till #stdout matches the given prompt.
- #rm_command!(output, command, **opts) ⇒ Object
-
#rm_prompt!(output, prompt: current_prompt, **opts) ⇒ Object
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/.
-
#sleep(duration) ⇒ Object
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.
- #stdin(content = String.new) ⇒ Object (also: #write)
- #stdout! ⇒ Object
-
#with_named_prompt(name) ⇒ Object
run something with a different named prompt.
-
#with_prompt(prompt) ⇒ Object
run something with a different prompt.
- #write_n(content = String.new) ⇒ Object
Instance Attribute Details
#channel ⇒ Object
Returns the value of attribute channel.
40 41 42 |
# File 'lib/net/ssh/cli.rb', line 40 def channel @channel end |
#logger ⇒ Object
Returns the value of attribute logger.
40 41 42 |
# File 'lib/net/ssh/cli.rb', line 40 def logger @logger end |
#net_ssh ⇒ Object Also known as: proxy
NET::SSH
359 360 361 |
# File 'lib/net/ssh/cli.rb', line 359 def net_ssh @net_ssh end |
#new_data ⇒ Object
Returns the value of attribute new_data.
40 41 42 |
# File 'lib/net/ssh/cli.rb', line 40 def new_data @new_data end |
#process_count ⇒ Object
Returns the value of attribute process_count.
40 41 42 |
# File 'lib/net/ssh/cli.rb', line 40 def process_count @process_count end |
#stdout ⇒ Object
Returns the value of attribute stdout.
40 41 42 |
# File 'lib/net/ssh/cli.rb', line 40 def stdout @stdout end |
Class Method Details
.start(**opts) ⇒ Object
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…”
28 29 30 |
# File 'lib/net/ssh/cli.rb', line 28 def self.start(**opts) Net::SSH::CLI::Session.new(**opts) end |
Instance Method Details
#close_channel ⇒ Object
406 407 408 409 |
# 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) ⇒ Object Also known as: command, exec
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
267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 |
# 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.} after cmd #{command.inspect} was sent" end |
#cmds(*commands, **opts) ⇒ Object Also known as: commands
Execute multiple cmds, see #cmd
288 289 290 |
# File 'lib/net/ssh/cli.rb', line 288 def cmds(*commands, **opts) commands.flatten.map { |command| [command, cmd(command, **opts)] } end |
#current_prompt ⇒ Object
fancy prompt|prompt handling methods
157 158 159 |
# File 'lib/net/ssh/cli.rb', line 157 def current_prompt with_prompts[-1] || default_prompt end |
#detect_prompt(seconds: 3) ⇒ Object
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
182 183 184 185 186 187 188 189 |
# 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) ⇒ Object
255 256 257 258 |
# File 'lib/net/ssh/cli.rb', line 255 def dialog(command, prompt, **opts) opts = opts.clone.merge(prompt: prompt) cmd(command, **opts) end |
#host ⇒ Object Also known as: hostname, to_s
337 338 339 |
# File 'lib/net/ssh/cli.rb', line 337 def host @net_ssh&.host end |
#impact(command, **opts) ⇒ Object
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”
328 329 330 |
# File 'lib/net/ssh/cli.rb', line 328 def impact(command, **opts) run_impact? ? cmd(command, **opts) : "skip: #{command.inspect}" end |
#impacts(*commands, **opts) ⇒ Object
same as #cmds but for #impact instead of #cmd
333 334 335 |
# File 'lib/net/ssh/cli.rb', line 333 def impacts(*commands, **opts) commands.flatten.map { |command| [command, impact(command, **opts)] } end |
#initialize(**opts) ⇒ Object
32 33 34 35 36 37 38 |
# File 'lib/net/ssh/cli.rb', line 32 def initialize(**opts) .merge!(opts) self.net_ssh = .delete(:net_ssh) self.logger = .delete(:logger) || Logger.new(STDOUT, level: Logger::WARN) self.process_count = 0 @new_data = String.new end |
#on_stdout(new_data) ⇒ Object
124 125 126 127 128 129 130 131 |
# 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 ⇒ Object
cli_channel
380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 |
# 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() 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 ⇒ Object
75 76 77 78 79 80 81 82 83 |
# File 'lib/net/ssh/cli.rb', line 75 def @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) ⇒ Object
don’t even think about nesting hashes here
86 87 88 |
# File 'lib/net/ssh/cli.rb', line 86 def (**opts) .merge!(opts) end |
#options=(opts) ⇒ Object
90 91 92 |
# File 'lib/net/ssh/cli.rb', line 90 def (opts) @options = ActiveSupport::HashWithIndifferentAccess.new(opts) end |
#process(time = process_time) ⇒ Object
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.
374 375 376 377 378 |
# 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. end |
#prompt_in_stdout? ⇒ Boolean
239 240 241 242 243 244 245 246 247 248 |
# 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 ⇒ Object
147 148 149 150 151 152 |
# File 'lib/net/ssh/cli.rb', line 147 def read process var = stdout! logger.debug { "#read: \n#{var}" } var end |
#read_for(seconds:) ⇒ Object
250 251 252 253 |
# 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) ⇒ Object
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%
219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 |
# 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) ⇒ Object
293 294 295 |
# 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) ⇒ Object
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/
301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 |
# 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) ⇒ Object
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)
348 349 350 351 352 353 354 |
# 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) ⇒ Object Also known as: write
133 134 135 136 137 138 139 140 |
# 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 |
#stdout! ⇒ Object
118 119 120 121 122 |
# File 'lib/net/ssh/cli.rb', line 118 def stdout! var = stdout self.stdout = String.new var end |
#with_named_prompt(name) ⇒ Object
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”)
171 172 173 174 175 176 177 |
# 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) ⇒ Object
run something with a different prompt
with_prompt(/(?<prompt>nroot)z/) do
cmd("sudo -i")
cmd("cat /etc/passwd")
end cmd(“exit”)
198 199 200 201 202 203 204 205 206 |
# 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_n(content = String.new) ⇒ Object
143 144 145 |
# File 'lib/net/ssh/cli.rb', line 143 def write_n(content = String.new) write content + "\n" end |