Module: Net::SSH::CLI

Included in:
Session
Defined in:
lib/net/ssh/cli.rb,
lib/net/ssh/cli/version.rb

Defined Under Namespace

Classes: Error, Session

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

Class Method Summary collapse

Instance Method Summary collapse

Instance Attribute Details

#channelObject

Returns the value of attribute channel.



39
40
41
# File 'lib/net/ssh/cli.rb', line 39

def channel
  @channel
end

#loggerObject

Returns the value of attribute logger.



39
40
41
# File 'lib/net/ssh/cli.rb', line 39

def logger
  @logger
end

#net_sshObject Also known as: proxy

NET::SSH



264
265
266
# File 'lib/net/ssh/cli.rb', line 264

def net_ssh
  @net_ssh
end

#new_dataObject

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_countObject

Returns the value of attribute process_count.



39
40
41
# File 'lib/net/ssh/cli.rb', line 39

def process_count
  @process_count
end

#stdoutObject

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_channelObject



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_promptObject

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

#hostObject 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)
  options.merge!(opts)
  self.net_ssh = options.delete(:net_ssh)
  self.logger = options.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_channelObject

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

#optionsObject



58
59
60
61
62
63
64
65
66
# File 'lib/net/ssh/cli.rb', line 58

def options
  @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



69
70
71
# File 'lib/net/ssh/cli.rb', line 69

def options!(**opts)
  options.merge!(opts)
end

#options=(opts) ⇒ Object



73
74
75
# File 'lib/net/ssh/cli.rb', line 73

def options=(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.



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.message
end

#readObject



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