opswalrus 1.0.40 → 1.0.43
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile.lock +1 -1
- data/lib/opswalrus/bootstrap.sh +1 -1
- data/lib/opswalrus/host.rb +58 -37
- data/lib/opswalrus/interaction_handlers.rb +94 -70
- data/lib/opswalrus/invocation.rb +5 -5
- data/lib/opswalrus/local_pty_backend.rb +25 -11
- data/lib/opswalrus/ops_file_script_dsl.rb +32 -32
- data/lib/opswalrus/runtime_environment.rb +12 -12
- data/lib/opswalrus/sshkit_ext.rb +6 -7
- data/lib/opswalrus/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c95b5c18a56463ccb9dd8db68875978eb57e3e5311a73825e27b1c893486d1ec
|
4
|
+
data.tar.gz: '05889e0f2398083e580406575914265488eb97ce98db67aeac92536dc6bc1294'
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8147ed4fa64cdd0c8125ef6c885fac72075fe4e211e8b31275b7961689d370f44ea10cde45f83df352e35dab4d06c1a4cadd063286bb34d9d29312f8a943213c
|
7
|
+
data.tar.gz: fdb7a00079382392fc1631f85a64f8939936e9731586fed8c9a63c42557f59540665889376f4e2deec382c08c39d32bab52ce9e62dabbfd50eba87bd1d1397be
|
data/Gemfile.lock
CHANGED
data/lib/opswalrus/bootstrap.sh
CHANGED
@@ -27,7 +27,7 @@ if [ -x $RTX ]; then
|
|
27
27
|
|
28
28
|
# make sure the latest opswalrus gem is installed
|
29
29
|
# todo: figure out how to install this differently, so that test versions will work
|
30
|
-
|
30
|
+
gem install opswalrus
|
31
31
|
# $GEM_CMD install opswalrus
|
32
32
|
$RTX reshim
|
33
33
|
|
data/lib/opswalrus/host.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
require "set"
|
2
2
|
require "sshkit"
|
3
|
+
require "stringio"
|
3
4
|
require "tempfile"
|
4
5
|
|
5
6
|
require_relative "interaction_handlers"
|
@@ -30,12 +31,12 @@ module OpsWalrus
|
|
30
31
|
# we know we're dealing with a package dependency reference, so we want to run an ops file contained within the bundle directory,
|
31
32
|
# therefore, we want to reference the specified ops file with respect to the bundle dir
|
32
33
|
when PackageDependencyReference
|
33
|
-
RemoteImportInvocationContext.new(@runtime_env, self, namespace_or_ops_file, true,
|
34
|
+
RemoteImportInvocationContext.new(@runtime_env, self, namespace_or_ops_file, true, ops_prompt_for_sudo_password: !!ssh_password)
|
34
35
|
|
35
36
|
# we know we're dealing with a directory reference or OpsFile reference outside of the bundle dir, so we want to reference
|
36
37
|
# the specified ops file with respect to the root directory, and not with respect to the bundle dir
|
37
38
|
when DirectoryReference, OpsFileReference
|
38
|
-
RemoteImportInvocationContext.new(@runtime_env, self, namespace_or_ops_file, false,
|
39
|
+
RemoteImportInvocationContext.new(@runtime_env, self, namespace_or_ops_file, false, ops_prompt_for_sudo_password: !!ssh_password)
|
39
40
|
end
|
40
41
|
|
41
42
|
invocation_context._invoke(*args, **kwargs)
|
@@ -56,7 +57,7 @@ module OpsWalrus
|
|
56
57
|
namespace_or_ops_file = @runtime_env.resolve_sibling_symbol(ops_file, symbol_name)
|
57
58
|
App.instance.trace "namespace_or_ops_file=#{namespace_or_ops_file.to_s}"
|
58
59
|
|
59
|
-
invocation_context = RemoteImportInvocationContext.new(@runtime_env, self, namespace_or_ops_file, false,
|
60
|
+
invocation_context = RemoteImportInvocationContext.new(@runtime_env, self, namespace_or_ops_file, false, ops_prompt_for_sudo_password: !!ssh_password)
|
60
61
|
invocation_context._invoke(*args, **kwargs)
|
61
62
|
end
|
62
63
|
methods_defined << symbol_name
|
@@ -143,7 +144,7 @@ module OpsWalrus
|
|
143
144
|
end
|
144
145
|
|
145
146
|
# returns the tuple: [stdout, stderr, exit_status]
|
146
|
-
def shell!(desc_or_cmd = nil, cmd = nil, block = nil, input: nil, log_level: nil)
|
147
|
+
def shell!(desc_or_cmd = nil, cmd = nil, block = nil, input: nil, log_level: nil, ops_prompt_for_sudo_password: false)
|
147
148
|
# description = nil
|
148
149
|
|
149
150
|
return ["", "", 0] if !desc_or_cmd && !cmd && !block # we were told to do nothing; like hitting enter at the bash prompt; we can do nothing successfully
|
@@ -171,14 +172,17 @@ module OpsWalrus
|
|
171
172
|
|
172
173
|
cmd_id = Random.uuid.split('-').first
|
173
174
|
# if App.instance.report_mode?
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
175
|
+
output_block = StringIO.open do |io|
|
176
|
+
io.print Style.blue(host)
|
177
|
+
io.print " (#{Style.blue(self.alias)})" if self.alias
|
178
|
+
io.print " | #{Style.magenta(description)}" if description
|
179
|
+
io.puts
|
180
|
+
io.print Style.yellow(cmd_id)
|
181
|
+
io.print Style.green.bold(" > ")
|
182
|
+
io.puts Style.yellow(cmd)
|
183
|
+
io.string
|
184
|
+
end
|
185
|
+
puts output_block
|
182
186
|
|
183
187
|
# puts Style.green("*" * 80)
|
184
188
|
# if self.alias
|
@@ -196,35 +200,40 @@ module OpsWalrus
|
|
196
200
|
out, err, exit_status = if App.instance.dry_run?
|
197
201
|
["", "", 0]
|
198
202
|
else
|
199
|
-
sshkit_cmd = execute_cmd(cmd, input:
|
203
|
+
sshkit_cmd = execute_cmd(cmd, input_mapping: input, ops_prompt_for_sudo_password: ops_prompt_for_sudo_password)
|
200
204
|
[sshkit_cmd.full_stdout, sshkit_cmd.full_stderr, sshkit_cmd.exit_status]
|
201
205
|
end
|
202
206
|
t2 = Time.now
|
203
207
|
seconds = t2 - t1
|
204
208
|
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
209
|
+
output_block = StringIO.open do |io|
|
210
|
+
if App.instance.info? || log_level == :info
|
211
|
+
io.puts Style.cyan(out)
|
212
|
+
io.puts Style.red(err)
|
213
|
+
elsif App.instance.debug? || log_level == :debug
|
214
|
+
io.puts Style.cyan(out)
|
215
|
+
io.puts Style.red(err)
|
216
|
+
elsif App.instance.trace? || log_level == :trace
|
217
|
+
io.puts Style.cyan(out)
|
218
|
+
io.puts Style.red(err)
|
219
|
+
end
|
220
|
+
io.print Style.yellow(cmd_id)
|
221
|
+
io.print Style.blue(" | Finished in #{seconds} seconds with exit status ")
|
222
|
+
if exit_status == 0
|
223
|
+
io.puts Style.green("#{exit_status} (#{exit_status == 0 ? 'success' : 'failure'})")
|
224
|
+
else
|
225
|
+
io.puts Style.red("#{exit_status} (#{exit_status == 0 ? 'success' : 'failure'})")
|
226
|
+
end
|
227
|
+
io.puts Style.green("*" * 80)
|
228
|
+
io.string
|
221
229
|
end
|
230
|
+
puts output_block
|
222
231
|
|
223
232
|
[out, err, exit_status]
|
224
233
|
end
|
225
234
|
|
226
235
|
# runs the specified ops command with the specified command arguments
|
227
|
-
def run_ops(ops_command, ops_command_options = nil, command_arguments, in_bundle_root_dir: true)
|
236
|
+
def run_ops(ops_command, ops_command_options = nil, command_arguments, in_bundle_root_dir: true, ops_prompt_for_sudo_password: false)
|
228
237
|
local_hostname_for_remote_host = if self.alias
|
229
238
|
"#{host} (#{self.alias})"
|
230
239
|
else
|
@@ -245,11 +254,11 @@ module OpsWalrus
|
|
245
254
|
cmd << " #{@tmp_bundle_root_dir}" if in_bundle_root_dir
|
246
255
|
cmd << " #{command_arguments}" unless command_arguments.empty?
|
247
256
|
|
248
|
-
shell!(cmd, log_level: :info)
|
257
|
+
shell!(cmd, log_level: :info, ops_prompt_for_sudo_password: ops_prompt_for_sudo_password)
|
249
258
|
end
|
250
259
|
|
251
260
|
def desc(msg)
|
252
|
-
puts msg.mustache(2) # we use two here, because one stack frame accounts for the call from the ops script into HostProxy#desc
|
261
|
+
puts Style.green(msg.mustache(2)) # we use two here, because one stack frame accounts for the call from the ops script into HostProxy#desc
|
253
262
|
end
|
254
263
|
|
255
264
|
def env(*args, **kwargs)
|
@@ -414,16 +423,28 @@ module OpsWalrus
|
|
414
423
|
@tmp_ssh_key_files = []
|
415
424
|
end
|
416
425
|
|
417
|
-
def execute(*args,
|
418
|
-
|
426
|
+
def execute(*args, input_mapping: nil, ops_prompt_for_sudo_password: false)
|
427
|
+
sudo_password_args = {}
|
428
|
+
sudo_password_args[:sudo_password] = ssh_password unless ops_prompt_for_sudo_password
|
429
|
+
sudo_password_args[:ops_sudo_password] = ssh_password if ops_prompt_for_sudo_password
|
430
|
+
@runtime_env.handle_input(input_mapping, **sudo_password_args) do |interaction_handler|
|
419
431
|
# @sshkit_backend.capture(*args, interaction_handler: interaction_handler, verbosity: SSHKit.config.output_verbosity)
|
432
|
+
App.instance.debug("Host#execute_cmd(#{args.inspect}) with input mappings #{interaction_handler.input_mappings.inspect} given sudo_password_args: #{sudo_password_args.inspect})")
|
420
433
|
@sshkit_backend.capture(*args, interaction_handler: interaction_handler)
|
421
434
|
end
|
422
435
|
end
|
423
436
|
|
424
|
-
def execute_cmd(*args,
|
425
|
-
|
426
|
-
|
437
|
+
def execute_cmd(*args, input_mapping: nil, ops_prompt_for_sudo_password: false)
|
438
|
+
# we only want one of the sudo password interaction handlers:
|
439
|
+
# if we're running an ops script on the remote host and we've passed the --pass flag in our invocation of the ops command on the remote host,
|
440
|
+
# then we want to specify the sudo password via the ops_sudo_password argument to #handle_input
|
441
|
+
# if we're running a command on the remote host via #shell!, and we aren't running the ops command with the --pass flag,
|
442
|
+
# then we want to specify the sudo password via the sudo_password argument to #handle_input
|
443
|
+
sudo_password_args = {}
|
444
|
+
sudo_password_args[:sudo_password] = ssh_password unless ops_prompt_for_sudo_password
|
445
|
+
sudo_password_args[:ops_sudo_password] = ssh_password if ops_prompt_for_sudo_password
|
446
|
+
@runtime_env.handle_input(input_mapping, **sudo_password_args) do |interaction_handler|
|
447
|
+
App.instance.debug("Host#execute_cmd(#{args.inspect}) with input mappings #{interaction_handler.input_mappings.inspect} given sudo_password_args: #{sudo_password_args.inspect})")
|
427
448
|
@sshkit_backend.execute_cmd(*args, interaction_handler: interaction_handler)
|
428
449
|
end
|
429
450
|
end
|
@@ -3,6 +3,8 @@ require 'sshkit'
|
|
3
3
|
module OpsWalrus
|
4
4
|
|
5
5
|
class ScopedMappingInteractionHandler
|
6
|
+
STANDARD_SUDO_PASSWORD_PROMPT = /\[sudo\] password for .*?:\s*/
|
7
|
+
|
6
8
|
attr_accessor :input_mappings # Hash[ String | Regex => String ]
|
7
9
|
|
8
10
|
# log_level is one of: :fatal, :error, :warn, :info, :debug, :trace
|
@@ -11,41 +13,48 @@ module OpsWalrus
|
|
11
13
|
@input_mappings = mapping
|
12
14
|
end
|
13
15
|
|
14
|
-
#
|
15
|
-
# when the given block returns, then the temporary mapping is removed from the interaction handler
|
16
|
-
# def with_sudo_password(password, &block)
|
17
|
-
# with_mapping({
|
18
|
-
# /\[sudo\] password for .*?:\s*/ => "#{password}\n",
|
19
|
-
# App::LOCAL_SUDO_PASSWORD_PROMPT => "#{password}\n",
|
20
|
-
# # /\s+/ => nil, # unnecessary
|
21
|
-
# }, &block)
|
22
|
-
# end
|
23
|
-
|
24
|
-
# sudo_password : String
|
16
|
+
# sudo_password : String | Nil
|
25
17
|
def self.mapping_for_sudo_password(sudo_password)
|
18
|
+
password_response = sudo_password && ::SSHKit::InteractionHandler::Password.new("#{sudo_password}\n")
|
26
19
|
{
|
27
|
-
|
28
|
-
|
20
|
+
STANDARD_SUDO_PASSWORD_PROMPT => password_response,
|
21
|
+
}
|
22
|
+
end
|
23
|
+
|
24
|
+
# sudo_password : String | Nil
|
25
|
+
def self.mapping_for_ops_sudo_prompt(sudo_password)
|
26
|
+
password_response = sudo_password && ::SSHKit::InteractionHandler::Password.new("#{sudo_password}\n")
|
27
|
+
{
|
28
|
+
App::LOCAL_SUDO_PASSWORD_PROMPT => password_response,
|
29
29
|
}
|
30
30
|
end
|
31
31
|
|
32
32
|
# temporarily adds the specified input mapping to the interaction handler while the given block is being evaluated
|
33
33
|
# when the given block returns, then the temporary mapping is removed from the interaction handler
|
34
34
|
#
|
35
|
-
# mapping : Hash[ String | Regex => String ]
|
36
|
-
def with_mapping(mapping, sudo_password
|
37
|
-
|
35
|
+
# mapping : Hash[ String | Regex => String ] | Nil
|
36
|
+
def with_mapping(mapping = nil, sudo_password: nil, ops_sudo_password: nil, inherit_existing_mappings: false)
|
37
|
+
new_mapping = inherit_existing_mappings ? @input_mappings : {}
|
38
38
|
|
39
|
-
|
39
|
+
if mapping
|
40
|
+
raise ArgumentError.new("mapping must be a Hash") unless mapping.is_a?(Hash)
|
41
|
+
new_mapping.merge!(mapping)
|
42
|
+
end
|
40
43
|
|
41
|
-
|
42
|
-
|
44
|
+
# ops_sudo_password takes precedence over sudo_password
|
45
|
+
password_mappings = if ops_sudo_password
|
46
|
+
ScopedMappingInteractionHandler.mapping_for_ops_sudo_prompt(ops_sudo_password).
|
47
|
+
merge(ScopedMappingInteractionHandler.mapping_for_sudo_password(nil))
|
48
|
+
elsif sudo_password
|
49
|
+
ScopedMappingInteractionHandler.mapping_for_sudo_password(sudo_password).
|
50
|
+
merge(ScopedMappingInteractionHandler.mapping_for_ops_sudo_prompt(nil))
|
43
51
|
end
|
52
|
+
new_mapping.merge!(password_mappings) if password_mappings
|
44
53
|
|
45
|
-
if
|
54
|
+
if new_mapping.empty?
|
46
55
|
yield self
|
47
56
|
else
|
48
|
-
yield ScopedMappingInteractionHandler.new(
|
57
|
+
yield ScopedMappingInteractionHandler.new(new_mapping, @log_level)
|
49
58
|
end
|
50
59
|
end
|
51
60
|
|
@@ -56,7 +65,18 @@ module OpsWalrus
|
|
56
65
|
@input_mappings.merge!(mapping)
|
57
66
|
end
|
58
67
|
|
59
|
-
|
68
|
+
# cmd, :stdout, data, stdin
|
69
|
+
# the return value from on_data is returned to Command#call_interaction_handler which is then returned verbatim
|
70
|
+
# to Command#on_stdout, which is then returned verbatim to the backend that called #on_stdout, and in my case
|
71
|
+
# that is LocalPty#handle_data_for_stdout.
|
72
|
+
# So, LocalPty#handle_data_for_stdout -> Command#on_stdout -> Command#call_interaction_handler -> ScopedMappingInteractionHandler#on_data
|
73
|
+
# which means that if I return that a password was emitted from this method, then back in LocalPty#handle_data_for_stdout
|
74
|
+
# I can discard the subsequent line that I read from stdout in order to read and immediately discard the password
|
75
|
+
# that this interaction handler emits.
|
76
|
+
#
|
77
|
+
# This method returns the data that is emitted to the response channel as a result of having processed the output
|
78
|
+
# from a command that the interaction handler was expecting.
|
79
|
+
def on_data(_command, stream_name, data, response_channel)
|
60
80
|
response_data = begin
|
61
81
|
first_matching_key_value_pair = @input_mappings.find {|k, _v| k === data }
|
62
82
|
first_matching_key_value_pair&.last
|
@@ -67,14 +87,18 @@ module OpsWalrus
|
|
67
87
|
else
|
68
88
|
debug(Style.cyan("Handling #{stream_name} message #{data}"))
|
69
89
|
debug(Style.cyan("Sending response #{response_data}"))
|
70
|
-
if
|
71
|
-
|
72
|
-
|
73
|
-
|
90
|
+
if response_channel.respond_to?(:send_data) # Net SSH Channel
|
91
|
+
App.instance.trace "writing: #{response_data.to_s} to Net SSH Channel"
|
92
|
+
response_channel.send_data(response_data.to_s)
|
93
|
+
elsif response_channel.respond_to?(:write) # Local IO (stdin)
|
94
|
+
App.instance.trace "writing: #{response_data.to_s} to pty stdin"
|
95
|
+
response_channel.write(response_data.to_s)
|
74
96
|
else
|
75
97
|
raise "Unable to write response data to channel #{channel.inspect} - does not support '#send_data' or '#write'"
|
76
98
|
end
|
77
99
|
end
|
100
|
+
|
101
|
+
response_data
|
78
102
|
end
|
79
103
|
|
80
104
|
private
|
@@ -89,49 +113,49 @@ module OpsWalrus
|
|
89
113
|
|
90
114
|
end
|
91
115
|
|
92
|
-
class PasswdInteractionHandler
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
end
|
105
|
-
|
106
|
-
class SudoPasswordMapper
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
end
|
119
|
-
|
120
|
-
class SudoPromptInteractionHandler
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
end
|
116
|
+
# class PasswdInteractionHandler
|
117
|
+
# def on_data(command, stream_name, data, channel)
|
118
|
+
# case data
|
119
|
+
# when '(current) UNIX password: '
|
120
|
+
# channel.send_data("old_pw\n")
|
121
|
+
# when 'Enter new UNIX password: ', 'Retype new UNIX password: '
|
122
|
+
# channel.send_data("new_pw\n")
|
123
|
+
# when 'passwd: password updated successfully'
|
124
|
+
# else
|
125
|
+
# raise "Unexpected stderr #{stderr}"
|
126
|
+
# end
|
127
|
+
# end
|
128
|
+
# end
|
129
|
+
|
130
|
+
# class SudoPasswordMapper
|
131
|
+
# def initialize(sudo_password)
|
132
|
+
# @sudo_password = sudo_password
|
133
|
+
# end
|
134
|
+
|
135
|
+
# def interaction_handler
|
136
|
+
# SSHKit::MappingInteractionHandler.new({
|
137
|
+
# /\[sudo\] password for .*?:\s*/ => "#{@sudo_password}\n",
|
138
|
+
# App::LOCAL_SUDO_PASSWORD_PROMPT => "#{@sudo_password}\n",
|
139
|
+
# # /\s+/ => nil, # unnecessary
|
140
|
+
# }, :info)
|
141
|
+
# end
|
142
|
+
# end
|
143
|
+
|
144
|
+
# class SudoPromptInteractionHandler
|
145
|
+
# def on_data(command, stream_name, data, channel)
|
146
|
+
# case data
|
147
|
+
# when /\[sudo\] password for/
|
148
|
+
# if channel.respond_to?(:send_data) # Net::SSH channel
|
149
|
+
# channel.send_data("conquer\n")
|
150
|
+
# elsif channel.respond_to?(:write) # IO
|
151
|
+
# channel.write("conquer\n")
|
152
|
+
# end
|
153
|
+
# when /\s+/
|
154
|
+
# nil
|
155
|
+
# else
|
156
|
+
# raise "Unexpected prompt: #{data} on stream #{stream_name} and channel #{channel.inspect}"
|
157
|
+
# end
|
158
|
+
# end
|
159
|
+
# end
|
136
160
|
|
137
161
|
end
|
data/lib/opswalrus/invocation.rb
CHANGED
@@ -26,7 +26,7 @@ module OpsWalrus
|
|
26
26
|
end
|
27
27
|
|
28
28
|
class RemoteImportInvocationContext < ImportInvocationContext
|
29
|
-
def initialize(runtime_env, host_proxy, namespace_or_ops_file, is_invocation_a_call_to_package_in_bundle_dir = false,
|
29
|
+
def initialize(runtime_env, host_proxy, namespace_or_ops_file, is_invocation_a_call_to_package_in_bundle_dir = false, ops_prompt_for_sudo_password: nil)
|
30
30
|
@runtime_env = runtime_env
|
31
31
|
@host_proxy = host_proxy
|
32
32
|
@initial_namespace_or_ops_file = @namespace_or_ops_file = namespace_or_ops_file
|
@@ -39,7 +39,7 @@ module OpsWalrus
|
|
39
39
|
@namespace_or_ops_file.basename
|
40
40
|
end
|
41
41
|
@method_chain = [initial_method_name]
|
42
|
-
@
|
42
|
+
@ops_prompt_for_sudo_password = ops_prompt_for_sudo_password
|
43
43
|
end
|
44
44
|
|
45
45
|
def method_missing(name, *args, **kwargs, &block)
|
@@ -123,12 +123,12 @@ module OpsWalrus
|
|
123
123
|
|
124
124
|
# invoke the ops command on the remote host to run the specified ops script on the remote host
|
125
125
|
ops_command_options = ""
|
126
|
-
ops_command_options << "--pass" if @
|
126
|
+
ops_command_options << "--pass" if @ops_prompt_for_sudo_password
|
127
127
|
ops_command_options << " --params #{remote_json_kwargs_tempfile_basename}" if remote_json_kwargs_tempfile_basename
|
128
128
|
retval = if ops_command_options.empty?
|
129
|
-
@host_proxy.run_ops(:run, remote_run_command_args)
|
129
|
+
@host_proxy.run_ops(:run, remote_run_command_args, ops_prompt_for_sudo_password: @ops_prompt_for_sudo_password)
|
130
130
|
else
|
131
|
-
@host_proxy.run_ops(:run, ops_command_options, remote_run_command_args)
|
131
|
+
@host_proxy.run_ops(:run, ops_command_options, remote_run_command_args, ops_prompt_for_sudo_password: @ops_prompt_for_sudo_password)
|
132
132
|
end
|
133
133
|
|
134
134
|
retval
|
@@ -1,6 +1,5 @@
|
|
1
1
|
require 'open3'
|
2
2
|
require 'pty'
|
3
|
-
require 'fileutils'
|
4
3
|
|
5
4
|
module SSHKit
|
6
5
|
|
@@ -14,6 +13,8 @@ module SSHKit
|
|
14
13
|
def execute_command(cmd)
|
15
14
|
output.log_command_start(cmd.with_redaction)
|
16
15
|
cmd.started = Time.now
|
16
|
+
# stderr_reader, stderr_writer = IO.pipe
|
17
|
+
# PTY.spawn(cmd.to_command, err: stderr_writer.fileno) do |stdout, stdin, pid|
|
17
18
|
PTY.spawn(cmd.to_command) do |stdout, stdin, pid|
|
18
19
|
stdout_thread = Thread.new do
|
19
20
|
# debug_log = StringIO.new
|
@@ -30,7 +31,7 @@ module SSHKit
|
|
30
31
|
# puts "nonblocking1. buffer=#{buffer} partial_buffer=#{partial_buffer}"
|
31
32
|
buffer = handle_data_for_stdout(output, cmd, buffer, stdin, false)
|
32
33
|
# puts "nonblocking2. buffer=#{buffer} partial_buffer=#{partial_buffer}"
|
33
|
-
rescue IO::WaitReadable, Errno::EAGAIN, Errno::EWOULDBLOCK
|
34
|
+
rescue IO::WaitReadable, Errno::EAGAIN, Errno::EWOULDBLOCK, IO::EAGAINWaitReadable
|
34
35
|
# puts "blocking. buffer=#{buffer} partial_buffer=#{partial_buffer}"
|
35
36
|
buffer = handle_data_for_stdout(output, cmd, buffer, stdin, true)
|
36
37
|
IO.select([stdout])
|
@@ -60,33 +61,46 @@ module SSHKit
|
|
60
61
|
end
|
61
62
|
stdout_thread.join
|
62
63
|
_pid, status = Process.wait2(pid)
|
64
|
+
# stderr_writer.close
|
65
|
+
# output.log_command_data(cmd, :stderr, stderr_reader.read)
|
63
66
|
cmd.exit_status = status.exitstatus
|
64
67
|
output.log_command_exit(cmd)
|
65
68
|
end
|
69
|
+
# ensure
|
70
|
+
# stderr_reader.close
|
66
71
|
end
|
67
72
|
|
68
73
|
# returns [complete lines, new buffer]
|
69
74
|
def split_buffer(buffer)
|
70
|
-
lines = buffer.split(/(\r\n)|\r|\n/)
|
75
|
+
# lines = buffer.split(/(\r\n)|\r|\n/)
|
76
|
+
lines = buffer.lines("\r\n").flat_map {|line| line.lines("\r") }.flat_map {|line| line.lines("\n") }
|
71
77
|
buffer = lines.pop
|
72
78
|
[lines, buffer]
|
73
79
|
end
|
74
80
|
|
81
|
+
# todo: we want to allow for cmd.on_stdout to invoke the interactionhandlers, but we want our interaction handler
|
82
|
+
# to be able to indicate that a password has been emitted, and therefore should be read back and omitted from the
|
83
|
+
# logged output because, per https://toasterlovin.com/using-the-pty-class-to-test-interactive-cli-apps-in-ruby/,
|
84
|
+
# the behavior of a PTY is to echo back any input was typed into the pseudoterminal, which means we will need to
|
85
|
+
# discard the input that we type in for password prompts, to ensure that the password is not logged as part
|
86
|
+
# of the stdout that we get back as we read from stdout of the spawned process
|
75
87
|
def handle_data_for_stdout(output, cmd, buffer, stdin, is_blocked)
|
76
|
-
# puts "handling data for stdout: #{buffer}"
|
77
|
-
|
78
88
|
# we're blocked on reading, so let's process the buffer
|
79
89
|
lines, buffer = split_buffer(buffer)
|
80
90
|
lines.each do |line|
|
81
|
-
|
82
|
-
|
83
|
-
|
91
|
+
::OpsWalrus::App.instance.trace("line=|>#{line}<|")
|
92
|
+
emitted_response_from_interaction_handler = cmd.on_stdout(stdin, line)
|
93
|
+
if emitted_response_from_interaction_handler.is_a?(::SSHKit::InteractionHandler::Password)
|
94
|
+
::OpsWalrus::App.instance.trace("emitted password #{emitted_response_from_interaction_handler}")
|
95
|
+
end
|
84
96
|
output.log_command_data(cmd, :stdout, line)
|
85
97
|
end
|
86
98
|
if is_blocked && buffer
|
87
|
-
|
88
|
-
|
89
|
-
|
99
|
+
::OpsWalrus::App.instance.trace("line=|>#{buffer}<|")
|
100
|
+
emitted_response_from_interaction_handler = cmd.on_stdout(stdin, buffer)
|
101
|
+
if emitted_response_from_interaction_handler.is_a?(::SSHKit::InteractionHandler::Password)
|
102
|
+
::OpsWalrus::App.instance.trace("emitted password #{emitted_response_from_interaction_handler}")
|
103
|
+
end
|
90
104
|
output.log_command_data(cmd, :stdout, buffer)
|
91
105
|
buffer = ""
|
92
106
|
end
|
@@ -191,7 +191,7 @@ module OpsWalrus
|
|
191
191
|
end
|
192
192
|
|
193
193
|
def desc(msg)
|
194
|
-
puts msg.mustache(1)
|
194
|
+
puts Style.green(msg.mustache(1))
|
195
195
|
end
|
196
196
|
|
197
197
|
def env(*keys)
|
@@ -224,8 +224,6 @@ module OpsWalrus
|
|
224
224
|
|
225
225
|
# returns the tuple: [stdout, stderr, exit_status]
|
226
226
|
def shell!(desc_or_cmd = nil, cmd = nil, block = nil, input: nil, log_level: nil)
|
227
|
-
# description = nil
|
228
|
-
|
229
227
|
return ["", "", 0] if !desc_or_cmd && !cmd && !block # we were told to do nothing; like hitting enter at the bash prompt; we can do nothing successfully
|
230
228
|
|
231
229
|
description = desc_or_cmd if cmd || block
|
@@ -245,22 +243,20 @@ module OpsWalrus
|
|
245
243
|
else
|
246
244
|
cmd
|
247
245
|
end
|
248
|
-
# cmd = WalrusLang.eval(cmd) if !block && cmd =~ /{{.*}}/
|
249
|
-
# cmd = WalrusLang.render(cmd, block.binding) if block && cmd =~ /{{.*}}/
|
250
|
-
|
251
246
|
#cmd = Shellwords.escape(cmd)
|
252
247
|
|
253
248
|
cmd_id = Random.uuid.split('-').first
|
254
249
|
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
250
|
+
output_block = StringIO.open do |io|
|
251
|
+
io.print Style.blue(@runtime_env.local_hostname)
|
252
|
+
io.print " | #{Style.magenta(description)}" if description
|
253
|
+
io.puts
|
254
|
+
io.print Style.yellow(cmd_id)
|
255
|
+
io.print Style.green.bold(" > ")
|
256
|
+
io.puts Style.yellow(cmd)
|
257
|
+
io.string
|
258
|
+
end
|
259
|
+
puts output_block
|
264
260
|
|
265
261
|
return unless cmd && !cmd.strip.empty?
|
266
262
|
|
@@ -278,24 +274,28 @@ module OpsWalrus
|
|
278
274
|
t2 = Time.now
|
279
275
|
seconds = t2 - t1
|
280
276
|
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
277
|
+
output_block = StringIO.open do |io|
|
278
|
+
if App.instance.info? || log_level == :info
|
279
|
+
io.puts Style.cyan(out)
|
280
|
+
io.puts Style.red(err)
|
281
|
+
elsif App.instance.debug? || log_level == :debug
|
282
|
+
io.puts Style.cyan(out)
|
283
|
+
io.puts Style.red(err)
|
284
|
+
elsif App.instance.trace? || log_level == :trace
|
285
|
+
io.puts Style.cyan(out)
|
286
|
+
io.puts Style.red(err)
|
287
|
+
end
|
288
|
+
io.print Style.yellow(cmd_id)
|
289
|
+
io.print Style.blue(" | Finished in #{seconds} seconds with exit status ")
|
290
|
+
if exit_status == 0
|
291
|
+
io.puts Style.green("#{exit_status} (#{exit_status == 0 ? 'success' : 'failure'})")
|
292
|
+
else
|
293
|
+
io.puts Style.red("#{exit_status} (#{exit_status == 0 ? 'success' : 'failure'})")
|
294
|
+
end
|
295
|
+
io.puts Style.green("*" * 80)
|
296
|
+
io.string
|
297
297
|
end
|
298
|
-
|
298
|
+
puts output_block
|
299
299
|
|
300
300
|
[out, err, exit_status]
|
301
301
|
end
|
@@ -258,8 +258,14 @@ module OpsWalrus
|
|
258
258
|
|
259
259
|
# input_mapping : Hash[ String | Regex => String ]
|
260
260
|
# sudo_password : String
|
261
|
-
def handle_input(input_mapping, sudo_password
|
262
|
-
@interaction_handler.with_mapping(
|
261
|
+
def handle_input(input_mapping, sudo_password: nil, ops_sudo_password: nil, inherit_existing_mappings: false, &block)
|
262
|
+
@interaction_handler.with_mapping(
|
263
|
+
input_mapping,
|
264
|
+
sudo_password: sudo_password,
|
265
|
+
ops_sudo_password: ops_sudo_password,
|
266
|
+
inherit_existing_mappings: inherit_existing_mappings,
|
267
|
+
&block
|
268
|
+
)
|
263
269
|
end
|
264
270
|
|
265
271
|
# configure sshkit globally
|
@@ -267,16 +273,10 @@ module OpsWalrus
|
|
267
273
|
SSHKit.config.use_format :blackhole
|
268
274
|
SSHKit.config.output_verbosity = :info
|
269
275
|
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
# SSHKit.config.use_format :pretty
|
275
|
-
# SSHKit.config.output_verbosity = :debug
|
276
|
-
# elsif app.trace?
|
277
|
-
# SSHKit.config.use_format :pretty
|
278
|
-
# SSHKit.config.output_verbosity = :debug
|
279
|
-
# end
|
276
|
+
if app.debug? || app.trace?
|
277
|
+
SSHKit.config.use_format :pretty
|
278
|
+
SSHKit.config.output_verbosity = :debug
|
279
|
+
end
|
280
280
|
|
281
281
|
SSHKit::Backend::Netssh.configure do |ssh|
|
282
282
|
ssh.pty = true # necessary for interaction with sudo on the remote host
|
data/lib/opswalrus/sshkit_ext.rb
CHANGED
@@ -7,6 +7,11 @@ require_relative 'local_non_blocking_backend'
|
|
7
7
|
require_relative 'local_pty_backend'
|
8
8
|
|
9
9
|
module SSHKit
|
10
|
+
module InteractionHandler
|
11
|
+
class Password < String
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
10
15
|
module Backend
|
11
16
|
class Abstract
|
12
17
|
# def execute(*args)
|
@@ -15,18 +20,12 @@ module SSHKit
|
|
15
20
|
# end
|
16
21
|
|
17
22
|
def execute_cmd(*args)
|
18
|
-
options = { verbosity: :
|
23
|
+
options = { verbosity: :info, strip: true, raise_on_non_zero_exit: false }.merge(args.extract_options!)
|
19
24
|
create_command_and_execute(args, options)
|
20
25
|
end
|
21
26
|
end
|
22
27
|
end
|
23
28
|
|
24
|
-
# module Formatter
|
25
|
-
# class Pretty < Abstract
|
26
|
-
|
27
|
-
# end
|
28
|
-
# end
|
29
|
-
|
30
29
|
module Runner
|
31
30
|
class Sequential < Abstract
|
32
31
|
def run_backend(host, &block)
|
data/lib/opswalrus/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: opswalrus
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0.
|
4
|
+
version: 1.0.43
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- David Ellis
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-09-
|
11
|
+
date: 2023-09-13 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: binding_of_caller
|