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.3.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, **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.
39 40 41 |
# File 'lib/net/ssh/cli.rb', line 39 def channel @channel end |
#logger ⇒ Object
Returns the value of attribute logger.
39 40 41 |
# File 'lib/net/ssh/cli.rb', line 39 def logger @logger end |
#net_ssh ⇒ Object Also known as: proxy
NET::SSH
264 265 266 |
# File 'lib/net/ssh/cli.rb', line 264 def net_ssh @net_ssh end |
#new_data ⇒ Object
Returns the value of attribute new_data.
39 40 41 |
# File 'lib/net/ssh/cli.rb', line 39 def new_data @new_data end |
#process_count ⇒ Object
Returns the value of attribute process_count.
39 40 41 |
# File 'lib/net/ssh/cli.rb', line 39 def process_count @process_count end |
#stdout ⇒ Object
Returns the value of attribute stdout.
39 40 41 |
# File 'lib/net/ssh/cli.rb', line 39 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…”
27 28 29 |
# File 'lib/net/ssh/cli.rb', line 27 def self.start(**opts) Net::SSH::CLI::Session.new(**opts) end |
Instance Method Details
#close_channel ⇒ Object
311 312 313 314 |
# File 'lib/net/ssh/cli.rb', line 311 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
206 207 208 209 210 211 212 213 214 215 216 217 |
# File 'lib/net/ssh/cli.rb', line 206 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 end |
#cmds(*commands, **opts) ⇒ Object Also known as: commands
221 222 223 |
# File 'lib/net/ssh/cli.rb', line 221 def cmds(*commands, **opts) commands.flatten.map { |command| [command, cmd(command, **opts)] } end |
#current_prompt ⇒ Object
fancy prompt|prompt handling methods
138 139 140 |
# File 'lib/net/ssh/cli.rb', line 138 def current_prompt with_prompts[-1] || default_prompt end |
#detect_prompt(seconds: 3) ⇒ Object
150 151 152 153 154 155 156 157 158 159 160 161 |
# File 'lib/net/ssh/cli.rb', line 150 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
199 200 201 202 |
# File 'lib/net/ssh/cli.rb', line 199 def dialog(command, prompt, **opts) opts = opts.clone.merge(prompt: prompt) cmd(command, opts) end |
#host ⇒ Object Also known as: hostname, to_s
255 256 257 |
# File 'lib/net/ssh/cli.rb', line 255 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”
246 247 248 |
# File 'lib/net/ssh/cli.rb', line 246 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
251 252 253 |
# File 'lib/net/ssh/cli.rb', line 251 def impacts(*commands, **opts) commands.flatten.map { |command| [command, impact(command, **opts)] } end |
#initialize(**opts) ⇒ Object
31 32 33 34 35 36 37 |
# File 'lib/net/ssh/cli.rb', line 31 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
105 106 107 108 109 110 111 112 113 114 |
# File 'lib/net/ssh/cli.rb', line 105 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
285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 |
# File 'lib/net/ssh/cli.rb', line 285 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
58 59 60 61 62 63 64 65 66 |
# File 'lib/net/ssh/cli.rb', line 58 def ||= 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
69 70 71 |
# File 'lib/net/ssh/cli.rb', line 69 def (**opts) .merge!(opts) end |
#options=(opts) ⇒ Object
73 74 75 |
# File 'lib/net/ssh/cli.rb', line 73 def (opts) = 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.
279 280 281 282 283 |
# File 'lib/net/ssh/cli.rb', line 279 def process(time = process_time) background_processing? ? sleep(time) : net_ssh.process(time) rescue IOError => error raise Error, error. end |
#read ⇒ Object
128 129 130 131 132 133 |
# File 'lib/net/ssh/cli.rb', line 128 def read process var = stdout! logger.debug("#read: \n#{var}") var end |
#read_for(seconds:) ⇒ Object
194 195 196 197 |
# File 'lib/net/ssh/cli.rb', line 194 def read_for(seconds:) process(seconds) read end |
#read_till(prompt: current_prompt, timeout: read_till_timeout, **_opts) ⇒ Object
174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 |
# File 'lib/net/ssh/cli.rb', line 174 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) 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, "Timeout after #{timeout}s, #{current_prompt.inspect} never matched #{stdout.inspect}" end process sleep 0.1 end end end read end |
#rm_command!(output, command, **opts) ⇒ Object
226 227 228 |
# File 'lib/net/ssh/cli.rb', line 226 def rm_command!(output, command, **opts) output[command + "\n"] = '' if rm_command?(opts) && output[command + "\n"] end |
#rm_prompt!(output, **opts) ⇒ Object
230 231 232 233 234 235 236 237 |
# File 'lib/net/ssh/cli.rb', line 230 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
99 100 101 102 103 |
# File 'lib/net/ssh/cli.rb', line 99 def stdout! var = stdout self.stdout = String.new var end |
#with_named_prompt(name) ⇒ Object
142 143 144 145 146 147 148 |
# File 'lib/net/ssh/cli.rb', line 142 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
164 165 166 167 168 169 170 171 172 |
# File 'lib/net/ssh/cli.rb', line 164 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
116 117 118 119 120 121 |
# File 'lib/net/ssh/cli.rb', line 116 def write(content = String.new) logger.debug { "#write #{content.inspect}" } channel.send_data content process content end |
#write_n(content = String.new) ⇒ Object
124 125 126 |
# File 'lib/net/ssh/cli.rb', line 124 def write_n(content = String.new) write content + "\n" end |