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.2'
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, &block) ⇒ 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
355 356 357 |
# File 'lib/net/ssh/cli.rb', line 355 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
402 403 404 405 |
# File 'lib/net/ssh/cli.rb', line 402 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
265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 |
# File 'lib/net/ssh/cli.rb', line 265 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:, rm_prompt:, rm_command:, 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
286 287 288 |
# File 'lib/net/ssh/cli.rb', line 286 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
180 181 182 183 184 185 186 187 |
# File 'lib/net/ssh/cli.rb', line 180 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
253 254 255 256 |
# File 'lib/net/ssh/cli.rb', line 253 def dialog(command, prompt, **opts) opts = opts.clone.merge(prompt:) cmd(command, **opts) end |
#host ⇒ Object Also known as: hostname, to_s
333 334 335 |
# File 'lib/net/ssh/cli.rb', line 333 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”
324 325 326 |
# File 'lib/net/ssh/cli.rb', line 324 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
329 330 331 |
# File 'lib/net/ssh/cli.rb', line 329 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
376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 |
# File 'lib/net/ssh/cli.rb', line 376 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 process until channel 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.
370 371 372 373 374 |
# File 'lib/net/ssh/cli.rb', line 370 def process(time = process_time) background_processing? ? sleep(time) : net_ssh.process(time) rescue IOError => error raise Error, error. end |
#prompt_in_stdout? ⇒ Boolean
237 238 239 240 241 242 243 244 245 246 |
# File 'lib/net/ssh/cli.rb', line 237 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
248 249 250 251 |
# File 'lib/net/ssh/cli.rb', line 248 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%
217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 |
# File 'lib/net/ssh/cli.rb', line 217 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? raise Error::ReadTillTimeout, "#{current_prompt.inspect} didn't match on #{stdout.inspect} within #{timeout}s" if timeout and soft_timeout < Time.now process sleep 0.01 # don't race for CPU end end end read end |
#rm_command!(output, command, **opts) ⇒ Object
291 292 293 |
# File 'lib/net/ssh/cli.rb', line 291 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/
299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 |
# File 'lib/net/ssh/cli.rb', line 299 def rm_prompt!(output, prompt: current_prompt, **opts) if rm_prompt?(**opts) && (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 |
#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)
344 345 346 347 348 349 350 |
# File 'lib/net/ssh/cli.rb', line 344 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, &block) ⇒ 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 |
# File 'lib/net/ssh/cli.rb', line 171 def with_named_prompt(name, &block) raise Error::UndefinedMatch, "unknown named_prompt #{name}" unless named_prompts[name] with_prompt(named_prompts[name], &block) 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”)
196 197 198 199 200 201 202 203 204 |
# File 'lib/net/ssh/cli.rb', line 196 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 |