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 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 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 )
- VERSION =
'1.6.0'
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, **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
348 349 350 |
# File 'lib/net/ssh/cli.rb', line 348 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
395 396 397 398 |
# File 'lib/net/ssh/cli.rb', line 395 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, **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 purpuse 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
257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 |
# File 'lib/net/ssh/cli.rb', line 257 def cmd(command, pre_read: true, rm_prompt: cmd_rm_prompt, rm_command: cmd_rm_command, prompt: current_prompt, **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 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
277 278 279 |
# File 'lib/net/ssh/cli.rb', line 277 def cmds(*commands, **opts) commands.flatten.map { |command| [command, cmd(command, **opts)] } end |
#current_prompt ⇒ Object
fancy prompt|prompt handling methods
147 148 149 |
# File 'lib/net/ssh/cli.rb', line 147 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
172 173 174 175 176 177 178 179 |
# File 'lib/net/ssh/cli.rb', line 172 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
245 246 247 248 |
# File 'lib/net/ssh/cli.rb', line 245 def dialog(command, prompt, **opts) opts = opts.clone.merge(prompt: prompt) cmd(command, **opts) end |
#host ⇒ Object Also known as: hostname, to_s
326 327 328 |
# File 'lib/net/ssh/cli.rb', line 326 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”
317 318 319 |
# File 'lib/net/ssh/cli.rb', line 317 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
322 323 324 |
# File 'lib/net/ssh/cli.rb', line 322 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
114 115 116 117 118 119 120 121 |
# File 'lib/net/ssh/cli.rb', line 114 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
369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 |
# File 'lib/net/ssh/cli.rb', line 369 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
67 68 69 70 71 72 73 74 75 |
# File 'lib/net/ssh/cli.rb', line 67 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
78 79 80 |
# File 'lib/net/ssh/cli.rb', line 78 def (**opts) .merge!(opts) end |
#options=(opts) ⇒ Object
82 83 84 |
# File 'lib/net/ssh/cli.rb', line 82 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.
363 364 365 366 367 |
# File 'lib/net/ssh/cli.rb', line 363 def process(time = process_time) background_processing? ? sleep(time) : net_ssh.process(time) rescue IOError => error raise Error, error. end |
#prompt_in_stdout? ⇒ Boolean
229 230 231 232 233 234 235 236 237 238 |
# File 'lib/net/ssh/cli.rb', line 229 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
137 138 139 140 141 142 |
# File 'lib/net/ssh/cli.rb', line 137 def read process var = stdout! logger.debug { "#read: \n#{var}" } var end |
#read_for(seconds:) ⇒ Object
240 241 242 243 |
# File 'lib/net/ssh/cli.rb', line 240 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%
209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 |
# File 'lib/net/ssh/cli.rb', line 209 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
282 283 284 |
# File 'lib/net/ssh/cli.rb', line 282 def rm_command!(output, command, **opts) output[command + "\n"] = '' if rm_command?(**opts) && output[command + "\n"] 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/
290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 |
# File 'lib/net/ssh/cli.rb', line 290 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)
337 338 339 340 341 342 343 |
# File 'lib/net/ssh/cli.rb', line 337 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
123 124 125 126 127 128 129 130 |
# File 'lib/net/ssh/cli.rb', line 123 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
108 109 110 111 112 |
# File 'lib/net/ssh/cli.rb', line 108 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”)
161 162 163 164 165 166 167 |
# File 'lib/net/ssh/cli.rb', line 161 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”)
188 189 190 191 192 193 194 195 196 |
# File 'lib/net/ssh/cli.rb', line 188 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
133 134 135 |
# File 'lib/net/ssh/cli.rb', line 133 def write_n(content = String.new) write content + "\n" end |