net-ssh-cli 1.3.0 → 1.7.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2e89dcb750ca04e0f861e75a15f7b43f5c18a9409857bd1c44e1458227d2e153
4
- data.tar.gz: f9e6ac90efa410f49401c04a4f66fa26e5977739005feba233b200d3336a41cf
3
+ metadata.gz: 6dd5a361663ac8c45079e1833688679c699f0022c1aa222f5e315992b2457fa3
4
+ data.tar.gz: d10d53f383ea82e100ff38ca46978fb27fbbaec1f49a8e6c4cd0e8cbca7abf9d
5
5
  SHA512:
6
- metadata.gz: d71c9a4d883426849d939b49505c84d336fdcf26a3bce7ef1cae364f903881d6de2654091da2061d0ebb50e806ab56fea71b090b591c0924388976944b0aff27
7
- data.tar.gz: a25df68ad93832c42767a88e6e14800160301e2ce36e4e938efb65954dbf2b8c5fdcd92a1cabab57b2ec06482cdf27a14936f101fa120914ae656fe828f0bd3c
6
+ metadata.gz: 3ab1b89f32f27604f5c04ed43b262f69860884be324077acc5802b5d0b03ebb544818ea53e36a8c04077a00a47b8e320aa7ae664653fd6eddeb3f6d53f0e30b9
7
+ data.tar.gz: 99bd97c5b61ea4853a3150047f8b88d6165e00a8e343b1a3ca746fd8648915555edcdc9bcbe0277568cf8d822e7622bdaa3a457604a7956bf9797d73a32eeef4
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- 2.6
1
+ 3.0
data/README.md CHANGED
@@ -79,12 +79,22 @@ It's the same as #cmd but for multiple commands.
79
79
  ```
80
80
 
81
81
  ### #dialog
82
+
83
+ Use this method to specify a differnt 'prompt' for once. This is perfect for interactive commands.
84
+
82
85
  ```ruby
83
86
  cli.dialog "echo 'are you sure?' && read -p 'yes|no>'", /\nyes|no>/
84
87
  # => "echo 'are you sure?' && read -p 'yes|no>'\nyes|no>"
85
88
  cli.cmd "yes"
86
89
  ```
87
90
 
91
+ ```ruby
92
+ cli.dialog "passwd", /Current Password:/i
93
+ cli.dialog "Old Password", /New Password:/i
94
+ cli.dialog "New Password", /Repeat Password:/i
95
+ cli.cmd "New Password"
96
+ ```
97
+
88
98
  ### #impact
89
99
 
90
100
  The very same as `#cmd` but it respects a flag whether to run commands with 'impact'.
@@ -148,10 +158,12 @@ Nearly everything can be configured.
148
158
  ### Callbacks
149
159
 
150
160
  The following callbacks are available
151
- - #before_open_channel
152
- - #after_open_channel
153
- - #before_on_data
154
- - #before_on_data
161
+ - before_open_channel
162
+ - after_open_channel
163
+ - before_on_stdout
164
+ - after_on_stdout
165
+ - before_on_stdin
166
+ - after_on_stdin
155
167
 
156
168
  ```ruby
157
169
  cli.before_open_channel do
@@ -166,6 +178,41 @@ cli.after_open_channel do
166
178
  end
167
179
  ```
168
180
 
181
+ Using the callbacks you can define a debugger which shows the `stdout` buffer content each time new data is received.
182
+
183
+ ```ruby
184
+ cli.after_on_stdout do
185
+ warn stdout
186
+ end
187
+ ```
188
+
189
+ ```ruby
190
+ cli.after_on_stdout do
191
+ puts "the following new data arrived on stdout #{new_data.inspect} from #{hostname}"
192
+ end
193
+ ```
194
+
195
+ or convert new lines between different OS
196
+ ```ruby
197
+ cli.after_on_stdout do
198
+ stdout.gsub!("\r\n", "\n")
199
+ end
200
+ ```
201
+
202
+ or hide passwords
203
+ ```ruby
204
+ cli.after_on_stdout do
205
+ stdout.gsub!(/password:\S+/, "<HIDDEN>")
206
+ end
207
+ ```
208
+
209
+ or change the stdin before sending it
210
+ ```ruby
211
+ cli.before_on_stdin do
212
+ content.gsub("\n\n", "\n")
213
+ end
214
+ ```
215
+
169
216
  ### #hostname #host #to_s
170
217
 
171
218
  ```ruby
data/lib/net/ssh/cli.rb CHANGED
@@ -17,6 +17,7 @@ module Net
17
17
  class OpenChannelTimeout < Error; end
18
18
  class ReadTillTimeout < Error; end
19
19
  class PromptDetection < Error; end
20
+ class CMD < Error; end
20
21
  end
21
22
 
22
23
  # Example
@@ -42,17 +43,26 @@ module Net
42
43
  default_prompt: /\n?^(\S+@.*)\z/, # the default prompt to search for
43
44
  cmd_rm_prompt: false, # whether the prompt should be removed in the output of #cmd
44
45
  cmd_rm_command: false, # whether the given command should be removed in the output of #cmd
46
+ cmd_rm_command_tail: "\n", # which format does the end of line return after a command has been submitted. Could be something like "ls\n" "ls\r\n" or "ls \n" (extra spaces)
45
47
  run_impact: false, # whether to run #impact commands. This might align with testing|development|production. example #impact("reboot")
46
48
  read_till_timeout: nil, # timeout for #read_till to find the match
49
+ read_till_hard_timeout: nil, # hard timeout for #read_till to find the match using Timeout.timeout(hard_timeout) {}. Might creates unpredicted sideffects
50
+ read_till_hard_timeout_factor: 1.2, # hard timeout factor in case read_till_hard_timeout is true
47
51
  named_prompts: ActiveSupport::HashWithIndifferentAccess.new, # you can used named prompts for #with_prompt {}
52
+ before_cmd_procs: ActiveSupport::HashWithIndifferentAccess.new, # procs to call before #cmd
53
+ after_cmd_procs: ActiveSupport::HashWithIndifferentAccess.new, # procs to call after #cmd
48
54
  before_on_stdout_procs: ActiveSupport::HashWithIndifferentAccess.new, # procs to call before data arrives from the underlying connection
49
55
  after_on_stdout_procs: ActiveSupport::HashWithIndifferentAccess.new, # procs to call after data arrives from the underlying connection
56
+ before_on_stdin_procs: ActiveSupport::HashWithIndifferentAccess.new, # procs to call before data is sent to the underlying channel
57
+ after_on_stdin_procs: ActiveSupport::HashWithIndifferentAccess.new, # procs to call after data is sent to the underlying channel
50
58
  before_open_channel_procs: ActiveSupport::HashWithIndifferentAccess.new, # procs to call before opening a channel
51
59
  after_open_channel_procs: ActiveSupport::HashWithIndifferentAccess.new, # procs to call after opening a channel, for example you could call #detect_prompt or #read_till
52
60
  open_channel_timeout: nil, # timeout to open the channel
53
61
  net_ssh_options: ActiveSupport::HashWithIndifferentAccess.new, # a wrapper for options to pass to Net::SSH.start in case net_ssh is undefined
54
62
  process_time: 0.00001, # how long #process is processing net_ssh#process or sleeping (waiting for something)
55
63
  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
64
+ on_stdout_processing: 100, # whether to optimize the on_stdout performance by calling #process #optimize_on_stdout-times in case more data arrives
65
+ sleep_procs: ActiveSupport::HashWithIndifferentAccess.new, # procs to call instead of Kernel.sleep(), perfect for async hooks
56
66
  )
57
67
 
58
68
  def options
@@ -107,19 +117,19 @@ module Net
107
117
  before_on_stdout_procs.each { |_name, a_proc| instance_eval(&a_proc) }
108
118
  stdout << new_data
109
119
  after_on_stdout_procs.each { |_name, a_proc| instance_eval(&a_proc) }
110
- self.process_count += 1
111
- 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
112
- self.process_count -= 1
120
+ optimise_stdout_processing
113
121
  stdout
114
122
  end
115
123
 
116
- def write(content = String.new)
124
+ def stdin(content = String.new)
117
125
  logger.debug { "#write #{content.inspect}" }
126
+ before_on_stdin_procs.each { |_name, a_proc| instance_eval(&a_proc) }
118
127
  channel.send_data content
119
128
  process
129
+ after_on_stdin_procs.each { |_name, a_proc| instance_eval(&a_proc) }
120
130
  content
121
131
  end
122
- alias stdin write
132
+ alias write stdin
123
133
 
124
134
  def write_n(content = String.new)
125
135
  write content + "\n"
@@ -128,7 +138,7 @@ module Net
128
138
  def read
129
139
  process
130
140
  var = stdout!
131
- logger.debug("#read: \n#{var}")
141
+ logger.debug { "#read: \n#{var}" }
132
142
  var
133
143
  end
134
144
 
@@ -139,6 +149,16 @@ module Net
139
149
  with_prompts[-1] || default_prompt
140
150
  end
141
151
 
152
+ # run something with a different named prompt
153
+ #
154
+ # named_prompts["root"] = /(?<prompt>\nroot)\z/
155
+ #
156
+ # with_named_prompt("root") do
157
+ # cmd("sudo -i")
158
+ # cmd("cat /etc/passwd")
159
+ # end
160
+ # cmd("exit")
161
+ #
142
162
  def with_named_prompt(name)
143
163
  raise Error::UndefinedMatch, "unknown named_prompt #{name}" unless named_prompts[name]
144
164
 
@@ -147,20 +167,25 @@ module Net
147
167
  end
148
168
  end
149
169
 
170
+ # tries to detect the prompt
171
+ # sends a "\n", waits for a X seconds, and uses the last line as prompt
172
+ # this won't work reliable if the prompt changes during the session
150
173
  def detect_prompt(seconds: 3)
151
174
  write_n
152
- future = Time.now + seconds
153
- while future > Time.now
154
- process
155
- sleep 0.1
156
- end
175
+ process(seconds)
157
176
  self.default_prompt = read[/\n?^.*\z/]
158
177
  raise Error::PromptDetection, "couldn't detect a prompt" unless default_prompt.present?
159
178
 
160
179
  default_prompt
161
180
  end
162
181
 
163
- # prove a block where the default prompt changes
182
+ # run something with a different prompt
183
+ #
184
+ # with_prompt(/(?<prompt>\nroot)\z/) do
185
+ # cmd("sudo -i")
186
+ # cmd("cat /etc/passwd")
187
+ # end
188
+ # cmd("exit")
164
189
  def with_prompt(prompt)
165
190
  logger.debug { "#with_prompt: #{current_prompt.inspect} => #{prompt.inspect}" }
166
191
  with_prompts << prompt
@@ -171,22 +196,48 @@ module Net
171
196
  logger.debug { "#with_prompt: => #{current_prompt.inspect}" }
172
197
  end
173
198
 
174
- def read_till(prompt: current_prompt, timeout: read_till_timeout, **_opts)
199
+ # continues to process the ssh connection till #stdout matches the given prompt.
200
+ # might raise a timeout error if a soft/hard timeout is given
201
+ # be carefull when using the hard_timeout, this is using the dangerous Timeout.timeout
202
+ # this gets really slow on large outputs, since the prompt will be searched in the whole output. Use \z in the regex if possible
203
+ #
204
+ # Optional named arguments:
205
+ # - prompt: expected to be a regex
206
+ # - timeout: nil or a number
207
+ # - hard_timeout: nil, true, or a number
208
+ # - hard_timeout_factor: nil, true, or a number
209
+ # - when hard_timeout == true, this will set the hard_timeout as (read_till_hard_timeout_factor * read_till_timeout), defaults to 1.2 = +20%
210
+ 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)
175
211
  raise Error::UndefinedMatch, 'no prompt given or default_prompt defined' unless prompt
176
-
177
- ::Timeout.timeout(timeout + 0.5, Error::ReadTillTimeout) do
178
- with_prompt(prompt) do
179
- soft_timeout = Time.now + timeout
180
- until stdout[current_prompt] do
181
- raise Error::ReadTillTimeout, "Timeout after #{timeout}s, #{current_prompt.inspect} never matched #{stdout.inspect}" if soft_timeout < Time.now
212
+ hard_timeout = (read_till_hard_timeout_factor * timeout) if timeout and hard_timeout == true
213
+ hard_timeout = nil if hard_timeout == true
214
+
215
+ with_prompt(prompt) do
216
+ ::Timeout.timeout(hard_timeout, Error::ReadTillTimeout, "#{current_prompt.inspect} didn't match on #{stdout.inspect} within #{hard_timeout}s") do
217
+ soft_timeout = Time.now + timeout if timeout
218
+ until prompt_in_stdout? do
219
+ if timeout and soft_timeout < Time.now
220
+ raise Error::ReadTillTimeout, "#{current_prompt.inspect} didn't match on #{stdout.inspect} within #{timeout}s"
221
+ end
182
222
  process
183
- sleep 0.1
223
+ sleep 0.01 # don't race for CPU
184
224
  end
185
225
  end
186
226
  end
187
227
  read
188
228
  end
189
229
 
230
+ def prompt_in_stdout?
231
+ case current_prompt
232
+ when Regexp
233
+ !!stdout[current_prompt]
234
+ when String
235
+ stdout.include?(current_prompt)
236
+ else
237
+ raise Net::SSH::CLI::Error, "prompt/current_prompt is not a String/Regex #{current_prompt.inspect}"
238
+ end
239
+ end
240
+
190
241
  def read_for(seconds:)
191
242
  process(seconds)
192
243
  read
@@ -194,40 +245,65 @@ module Net
194
245
 
195
246
  def dialog(command, prompt, **opts)
196
247
  opts = opts.clone.merge(prompt: prompt)
197
- cmd(command, opts)
248
+ cmd(command, **opts)
198
249
  end
199
250
 
200
- # 'read' first on purpuse as a feature. once you cmd you ignore what happend before. otherwise use read|write directly.
201
- # this should avoid many horrible state issues where the prompt is not the last prompt
251
+ # send a command and get the output as return value
252
+ # 1. sends the given command to the ssh connection channel
253
+ # 2. continues to process the ssh connection until the prompt is found in the stdout
254
+ # 3. prepares the output using your callbacks
255
+ # 4. returns the output of your command
256
+ # Hint: 'read' first on purpuse as a feature. once you cmd you ignore what happend before. otherwise use read|write directly.
257
+ # this should avoid many horrible state issues where the prompt is not the last prompt
202
258
  def cmd(command, pre_read: true, rm_prompt: cmd_rm_prompt, rm_command: cmd_rm_command, prompt: current_prompt, **opts)
203
259
  opts = opts.clone.merge(pre_read: pre_read, rm_prompt: rm_prompt, rm_command: rm_command, prompt: prompt)
204
260
  if pre_read
205
261
  pre_read_data = read
206
262
  logger.debug { "#cmd ignoring pre-command output: #{pre_read_data.inspect}" } if pre_read_data.present?
207
263
  end
264
+ before_cmd_procs.each { |_name, a_proc| instance_eval(&a_proc) }
208
265
  write_n command
209
- output = read_till(opts)
210
- rm_prompt!(output, opts)
211
- rm_command!(output, command, opts)
266
+ output = read_till(**opts)
267
+ rm_prompt!(output, **opts)
268
+ rm_command!(output, command, **opts)
269
+ after_cmd_procs.each { |_name, a_proc| instance_eval(&a_proc) }
212
270
  output
271
+ rescue Error::ReadTillTimeout => error
272
+ raise Error::CMD, "#{error.message} after cmd #{command.inspect} was sent"
213
273
  end
214
274
  alias command cmd
215
275
  alias exec cmd
216
276
 
277
+ # Execute multiple cmds, see #cmd
217
278
  def cmds(*commands, **opts)
218
279
  commands.flatten.map { |command| [command, cmd(command, **opts)] }
219
280
  end
220
281
  alias commands cmds
221
282
 
222
283
  def rm_command!(output, command, **opts)
223
- output[command + "\n"] = '' if rm_command?(opts) && output[command + "\n"]
284
+ output[command + cmd_rm_command_tail] = '' if rm_command?(**opts) && output[command + cmd_rm_command_tail]
224
285
  end
225
286
 
226
- def rm_prompt!(output, **opts)
227
- if rm_prompt?(opts)
228
- prompt = opts[:prompt] || current_prompt
287
+ # removes the prompt from the given output
288
+ # prompt should contain a named match 'prompt' /(?<prompt>.*something.*)\z/
289
+ # for backwards compatibility it also tries to replace the first match of the prompt /(something)\z/
290
+ # it removes the whole match if no matches are given /something\z/
291
+ def rm_prompt!(output, prompt: current_prompt, **opts)
292
+ if rm_prompt?(**opts)
229
293
  if output[prompt]
230
- prompt.is_a?(Regexp) ? output[prompt, 1] = '' : output[prompt] = ''
294
+ case prompt
295
+ when String then output[prompt] = ''
296
+ when Regexp
297
+ if prompt.names.include?("prompt")
298
+ output[prompt, "prompt"] = ''
299
+ else
300
+ begin
301
+ output[prompt, 1] = ''
302
+ rescue IndexError
303
+ output[prompt] = ''
304
+ end
305
+ end
306
+ end
231
307
  end
232
308
  end
233
309
  end
@@ -254,6 +330,19 @@ module Net
254
330
  alias hostname host
255
331
  alias to_s host
256
332
 
333
+ # if #sleep_procs are set, they will be called instead of Kernel.sleep
334
+ # great for async
335
+ # .sleep_procs["async"] = proc do |duration| async_reactor.sleep(duration) end
336
+ #
337
+ # cli.sleep(1)
338
+ def sleep(duration)
339
+ if sleep_procs.any?
340
+ sleep_procs.each { |_name, a_proc| instance_exec(duration, &a_proc) }
341
+ else
342
+ Kernel.sleep(duration)
343
+ end
344
+ end
345
+
257
346
  ## NET::SSH
258
347
  #
259
348
 
@@ -326,6 +415,17 @@ module Net
326
415
  def rm_command?(**opts)
327
416
  opts[:rm_cmd].nil? ? cmd_rm_command : opts[:rm_cmd]
328
417
  end
418
+
419
+ # when new data is beeing received, likely more data will arrive - this improves the performance by a large factor
420
+ # but on a lot of data, this leads to a stack level too deep
421
+ # therefore it is limited to max #on_stdout_processing
422
+ # the bigger on_stdout_processing, the closer we get to a stack level too deep
423
+ def optimise_stdout_processing
424
+ self.process_count += 1
425
+ process unless process_count > on_stdout_processing
426
+ ensure
427
+ self.process_count -= 1
428
+ end
329
429
  end
330
430
  end
331
431
  end
@@ -3,7 +3,7 @@
3
3
  module Net
4
4
  module SSH
5
5
  module CLI
6
- VERSION = '1.3.0'
6
+ VERSION = '1.7.0'
7
7
  end
8
8
  end
9
9
  end
data/net-ssh-cli.gemspec CHANGED
@@ -39,7 +39,7 @@ Gem::Specification.new do |spec|
39
39
 
40
40
  spec.add_development_dependency 'bundler'
41
41
  spec.add_development_dependency 'bump'
42
- spec.add_development_dependency 'rake', '~> 10.0'
42
+ spec.add_development_dependency 'rake', '~> 13.0'
43
43
  spec.add_development_dependency 'rspec', '~> 3.0'
44
44
  spec.add_dependency 'activesupport', '>= 4.0'
45
45
  spec.add_dependency 'net-ssh', '>= 4.0'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: net-ssh-cli
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.0
4
+ version: 1.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Fabian Stillhart
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2019-07-15 00:00:00.000000000 Z
11
+ date: 2021-02-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -44,14 +44,14 @@ dependencies:
44
44
  requirements:
45
45
  - - "~>"
46
46
  - !ruby/object:Gem::Version
47
- version: '10.0'
47
+ version: '13.0'
48
48
  type: :development
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
- version: '10.0'
54
+ version: '13.0'
55
55
  - !ruby/object:Gem::Dependency
56
56
  name: rspec
57
57
  requirement: !ruby/object:Gem::Requirement
@@ -140,7 +140,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
140
140
  - !ruby/object:Gem::Version
141
141
  version: '0'
142
142
  requirements: []
143
- rubygems_version: 3.0.4
143
+ rubygems_version: 3.2.3
144
144
  signing_key:
145
145
  specification_version: 4
146
146
  summary: 'Net::SSH::CLI: A library to handle CLI Sessions'