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 +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'
|