net-ssh-cli 1.3.0 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.ruby-version +1 -1
- data/README.md +51 -4
- data/lib/net/ssh/cli.rb +131 -31
- data/lib/net/ssh/cli/version.rb +1 -1
- data/net-ssh-cli.gemspec +1 -1
- metadata +5 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6dd5a361663ac8c45079e1833688679c699f0022c1aa222f5e315992b2457fa3
|
4
|
+
data.tar.gz: d10d53f383ea82e100ff38ca46978fb27fbbaec1f49a8e6c4cd0e8cbca7abf9d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3ab1b89f32f27604f5c04ed43b262f69860884be324077acc5802b5d0b03ebb544818ea53e36a8c04077a00a47b8e320aa7ae664653fd6eddeb3f6d53f0e30b9
|
7
|
+
data.tar.gz: 99bd97c5b61ea4853a3150047f8b88d6165e00a8e343b1a3ca746fd8648915555edcdc9bcbe0277568cf8d822e7622bdaa3a457604a7956bf9797d73a32eeef4
|
data/.ruby-version
CHANGED
@@ -1 +1 @@
|
|
1
|
-
|
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
|
-
-
|
152
|
-
-
|
153
|
-
-
|
154
|
-
-
|
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
|
-
|
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
|
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
|
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
|
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
|
-
|
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
|
-
#
|
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
|
-
|
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
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
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.
|
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
|
-
#
|
201
|
-
#
|
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 +
|
284
|
+
output[command + cmd_rm_command_tail] = '' if rm_command?(**opts) && output[command + cmd_rm_command_tail]
|
224
285
|
end
|
225
286
|
|
226
|
-
|
227
|
-
|
228
|
-
|
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
|
-
|
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
|
data/lib/net/ssh/cli/version.rb
CHANGED
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', '~>
|
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.
|
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:
|
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: '
|
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: '
|
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.
|
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'
|