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.5.0'

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Instance Attribute Details

#channelObject

Returns the value of attribute channel.



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

def channel
  @channel
end

#loggerObject

Returns the value of attribute logger.



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

def logger
  @logger
end

#net_sshObject Also known as: proxy

NET::SSH



267
268
269
# File 'lib/net/ssh/cli.rb', line 267

def net_ssh
  @net_ssh
end

#new_dataObject

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_countObject

Returns the value of attribute process_count.



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

def process_count
  @process_count
end

#stdoutObject

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_channelObject



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.message} 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_promptObject

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

#hostObject 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)
  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



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_channelObject

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

#optionsObject



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

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



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

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

#options=(opts) ⇒ Object



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

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.



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

#readObject



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