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 named_prompts: ActiveSupport::HashWithIndifferentAccess.new, # you can used named prompts for #with_prompt {} 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_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 )
- VERSION =
'1.5.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)
‘read’ first on purpuse as a feature.
- #cmds(*commands, **opts) ⇒ Object (also: #commands)
-
#current_prompt ⇒ Object
fancy prompt|prompt handling methods.
- #detect_prompt(seconds: 3) ⇒ Object
- #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.
- #read ⇒ Object
- #read_for(seconds:) ⇒ Object
- #read_till(prompt: current_prompt, timeout: read_till_timeout, **_opts) ⇒ Object
- #rm_command!(output, command, **opts) ⇒ Object
- #rm_prompt!(output, **opts) ⇒ Object
- #stdout! ⇒ Object
- #with_named_prompt(name) ⇒ Object
-
#with_prompt(prompt) ⇒ Object
prove a block where the default prompt changes.
- #write(content = String.new) ⇒ Object (also: #stdin)
- #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
267 268 269 |
# File 'lib/net/ssh/cli.rb', line 267 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
314 315 316 317 |
# File 'lib/net/ssh/cli.rb', line 314 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
‘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
207 208 209 210 211 212 213 214 215 216 217 218 219 220 |
# File 'lib/net/ssh/cli.rb', line 207 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 write_n command output = read_till(**opts) rm_prompt!(output, **opts) rm_command!(output, command, **opts) output rescue Error::ReadTillTimeout => error raise Error::CMD, "#{error.} after cmd #{command.inspect} was sent" end |
#cmds(*commands, **opts) ⇒ Object Also known as: commands
224 225 226 |
# File 'lib/net/ssh/cli.rb', line 224 def cmds(*commands, **opts) commands.flatten.map { |command| [command, cmd(command, **opts)] } end |
#current_prompt ⇒ Object
fancy prompt|prompt handling methods
139 140 141 |
# File 'lib/net/ssh/cli.rb', line 139 def current_prompt with_prompts[-1] || default_prompt end |
#detect_prompt(seconds: 3) ⇒ Object
151 152 153 154 155 156 157 158 159 160 161 162 |
# File 'lib/net/ssh/cli.rb', line 151 def detect_prompt(seconds: 3) write_n future = Time.now + seconds while future > Time.now process sleep 0.1 end 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
200 201 202 203 |
# File 'lib/net/ssh/cli.rb', line 200 def dialog(command, prompt, **opts) opts = opts.clone.merge(prompt: prompt) cmd(command, **opts) end |
#host ⇒ Object Also known as: hostname, to_s
258 259 260 |
# File 'lib/net/ssh/cli.rb', line 258 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”
249 250 251 |
# File 'lib/net/ssh/cli.rb', line 249 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
254 255 256 |
# File 'lib/net/ssh/cli.rb', line 254 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
106 107 108 109 110 111 112 113 114 115 |
# File 'lib/net/ssh/cli.rb', line 106 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) } self.process_count += 1 process unless process_count > 100 # if we receive data, we probably receive more - improves performance - but on a lot of data, this leads to a stack level too deep self.process_count -= 1 stdout end |
#open_channel ⇒ Object
cli_channel
288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 |
# File 'lib/net/ssh/cli.rb', line 288 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
59 60 61 62 63 64 65 66 67 |
# File 'lib/net/ssh/cli.rb', line 59 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
70 71 72 |
# File 'lib/net/ssh/cli.rb', line 70 def (**opts) .merge!(opts) end |
#options=(opts) ⇒ Object
74 75 76 |
# File 'lib/net/ssh/cli.rb', line 74 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.
282 283 284 285 286 |
# File 'lib/net/ssh/cli.rb', line 282 def process(time = process_time) background_processing? ? sleep(time) : net_ssh.process(time) rescue IOError => error raise Error, error. end |
#read ⇒ Object
129 130 131 132 133 134 |
# File 'lib/net/ssh/cli.rb', line 129 def read process var = stdout! logger.debug("#read: \n#{var}") var end |
#read_for(seconds:) ⇒ Object
195 196 197 198 |
# File 'lib/net/ssh/cli.rb', line 195 def read_for(seconds:) process(seconds) read end |
#read_till(prompt: current_prompt, timeout: read_till_timeout, **_opts) ⇒ Object
175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 |
# File 'lib/net/ssh/cli.rb', line 175 def read_till(prompt: current_prompt, timeout: read_till_timeout, **_opts) raise Error::UndefinedMatch, 'no prompt given or default_prompt defined' unless prompt hard_timeout = timeout hard_timeout += 0.5 if timeout ::Timeout.timeout(hard_timeout, Error::ReadTillTimeout, "#{current_prompt.inspect} didn't match on #{stdout.inspect} within #{timeout}s") do with_prompt(prompt) do soft_timeout = Time.now + timeout if timeout until stdout[current_prompt] 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.1 end end end read end |
#rm_command!(output, command, **opts) ⇒ Object
229 230 231 |
# File 'lib/net/ssh/cli.rb', line 229 def rm_command!(output, command, **opts) output[command + "\n"] = '' if rm_command?(**opts) && output[command + "\n"] end |
#rm_prompt!(output, **opts) ⇒ Object
233 234 235 236 237 238 239 240 |
# File 'lib/net/ssh/cli.rb', line 233 def rm_prompt!(output, **opts) if rm_prompt?(**opts) prompt = opts[:prompt] || current_prompt if output[prompt] prompt.is_a?(Regexp) ? output[prompt, 1] = '' : output[prompt] = '' end end end |
#stdout! ⇒ Object
100 101 102 103 104 |
# File 'lib/net/ssh/cli.rb', line 100 def stdout! var = stdout self.stdout = String.new var end |
#with_named_prompt(name) ⇒ Object
143 144 145 146 147 148 149 |
# File 'lib/net/ssh/cli.rb', line 143 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
prove a block where the default prompt changes
165 166 167 168 169 170 171 172 173 |
# File 'lib/net/ssh/cli.rb', line 165 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) ⇒ Object Also known as: stdin
117 118 119 120 121 122 |
# File 'lib/net/ssh/cli.rb', line 117 def write(content = String.new) logger.debug { "#write #{content.inspect}" } channel.send_data content process content end |
#write_n(content = String.new) ⇒ Object
125 126 127 |
# File 'lib/net/ssh/cli.rb', line 125 def write_n(content = String.new) write content + "\n" end |