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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 43969fb5bd89993b9f4fb4c127d8a63f7d074d79d08104609b3a43d4df3f6457
4
- data.tar.gz: beca6e40dcabbe38815ef6ac2fe6581895ba8ea2dcb27ab314ad22d5ff2d47cd
3
+ metadata.gz: c95b5c18a56463ccb9dd8db68875978eb57e3e5311a73825e27b1c893486d1ec
4
+ data.tar.gz: '05889e0f2398083e580406575914265488eb97ce98db67aeac92536dc6bc1294'
5
5
  SHA512:
6
- metadata.gz: be45244077503ac67da072c8d41cad8800553cb9dc243d49904de29588c20a93317543b3d2d2d97a8c7a74ca0a1abc6c0d2fa5d4187b1fe34d863e1e3e558218
7
- data.tar.gz: 3cd42b3c126aef7dcec443c7f14fe9ee308da1d153148430b2413fe4166c4a214c43b0e368b485a4b3dd6cddf9c3047f778e55dbd4b99dc79697d75eaf910c72
6
+ metadata.gz: 8147ed4fa64cdd0c8125ef6c885fac72075fe4e211e8b31275b7961689d370f44ea10cde45f83df352e35dab4d06c1a4cadd063286bb34d9d29312f8a943213c
7
+ data.tar.gz: fdb7a00079382392fc1631f85a64f8939936e9731586fed8c9a63c42557f59540665889376f4e2deec382c08c39d32bab52ce9e62dabbfd50eba87bd1d1397be
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- opswalrus (1.0.40)
4
+ opswalrus (1.0.43)
5
5
  bcrypt_pbkdf (~> 1.1)
6
6
  binding_of_caller (~> 1.0)
7
7
  citrus (~> 3.0)
@@ -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
- # gem install opswalrus
30
+ gem install opswalrus
31
31
  # $GEM_CMD install opswalrus
32
32
  $RTX reshim
33
33
 
@@ -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, prompt_for_sudo_password: !!ssh_password)
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, prompt_for_sudo_password: !!ssh_password)
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, prompt_for_sudo_password: !!ssh_password)
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
- puts Style.green("*" * 80)
175
- print Style.blue(host)
176
- print " (#{Style.blue(self.alias)})" if self.alias
177
- print " | #{Style.magenta(description)}" if description
178
- puts
179
- print Style.yellow(cmd_id)
180
- print Style.green.bold(" > ")
181
- puts Style.yellow(cmd)
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: 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
- if App.instance.info? || log_level == :info
206
- puts Style.cyan(out)
207
- puts Style.red(err)
208
- elsif App.instance.debug? || log_level == :debug
209
- puts Style.cyan(out)
210
- puts Style.red(err)
211
- elsif App.instance.trace? || log_level == :trace
212
- puts Style.cyan(out)
213
- puts Style.red(err)
214
- end
215
- print Style.yellow(cmd_id)
216
- print Style.blue(" | Finished in #{seconds} seconds with exit status ")
217
- if exit_status == 0
218
- puts Style.green("#{exit_status} (#{exit_status == 0 ? 'success' : 'failure'})")
219
- else
220
- puts Style.red("#{exit_status} (#{exit_status == 0 ? 'success' : 'failure'})")
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, input: nil)
418
- @runtime_env.handle_input(input, ssh_password) do |interaction_handler|
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, input: nil)
425
- @runtime_env.handle_input(input, ssh_password) do |interaction_handler|
426
- # @sshkit_backend.execute_cmd(*args, interaction_handler: interaction_handler, verbosity: SSHKit.config.output_verbosity)
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
- # temporarily adds a sudo password mapping to the interaction handler while the given block is being evaluated
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
- /\[sudo\] password for .*?:\s*/ => "#{sudo_password}\n",
28
- App::LOCAL_SUDO_PASSWORD_PROMPT => "#{sudo_password}\n",
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 = nil)
37
- mapping ||= {}
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
- raise ArgumentError.new("mapping must be a Hash") unless mapping.is_a?(Hash)
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
- if sudo_password
42
- mapping.merge!(ScopedMappingInteractionHandler.mapping_for_sudo_password(sudo_password))
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 mapping.empty?
54
+ if new_mapping.empty?
46
55
  yield self
47
56
  else
48
- yield ScopedMappingInteractionHandler.new(@input_mappings.merge(mapping), @log_level)
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
- def on_data(_command, stream_name, data, channel)
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 channel.respond_to?(:send_data) # Net SSH Channel
71
- channel.send_data(response_data)
72
- elsif channel.respond_to?(:write) # Local IO
73
- channel.write(response_data)
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
- def on_data(command, stream_name, data, channel)
94
- case data
95
- when '(current) UNIX password: '
96
- channel.send_data("old_pw\n")
97
- when 'Enter new UNIX password: ', 'Retype new UNIX password: '
98
- channel.send_data("new_pw\n")
99
- when 'passwd: password updated successfully'
100
- else
101
- raise "Unexpected stderr #{stderr}"
102
- end
103
- end
104
- end
105
-
106
- class SudoPasswordMapper
107
- def initialize(sudo_password)
108
- @sudo_password = sudo_password
109
- end
110
-
111
- def interaction_handler
112
- SSHKit::MappingInteractionHandler.new({
113
- /\[sudo\] password for .*?:\s*/ => "#{@sudo_password}\n",
114
- App::LOCAL_SUDO_PASSWORD_PROMPT => "#{@sudo_password}\n",
115
- # /\s+/ => nil, # unnecessary
116
- }, :info)
117
- end
118
- end
119
-
120
- class SudoPromptInteractionHandler
121
- def on_data(command, stream_name, data, channel)
122
- case data
123
- when /\[sudo\] password for/
124
- if channel.respond_to?(:send_data) # Net::SSH channel
125
- channel.send_data("conquer\n")
126
- elsif channel.respond_to?(:write) # IO
127
- channel.write("conquer\n")
128
- end
129
- when /\s+/
130
- nil
131
- else
132
- raise "Unexpected prompt: #{data} on stream #{stream_name} and channel #{channel.inspect}"
133
- end
134
- end
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
@@ -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, prompt_for_sudo_password: nil)
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
- @prompt_for_sudo_password = prompt_for_sudo_password
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 @prompt_for_sudo_password
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
- # puts "1" * 80
82
- # puts line
83
- cmd.on_stdout(stdin, line)
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
- # puts "2" * 80
88
- # puts buffer
89
- cmd.on_stdout(stdin, buffer)
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
- # if App.instance.report_mode?
256
- puts Style.green("*" * 80)
257
- print Style.blue(@runtime_env.local_hostname)
258
- print " | #{Style.magenta(description)}" if description
259
- puts
260
- print Style.yellow(cmd_id)
261
- print Style.green.bold(" > ")
262
- puts Style.yellow(cmd)
263
- # end
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
- if App.instance.info? || log_level == :info
282
- puts Style.cyan(out)
283
- puts Style.red(err)
284
- elsif App.instance.debug? || log_level == :debug
285
- puts Style.cyan(out)
286
- puts Style.red(err)
287
- elsif App.instance.trace? || log_level == :trace
288
- puts Style.cyan(out)
289
- puts Style.red(err)
290
- end
291
- print Style.yellow(cmd_id)
292
- print Style.blue(" | Finished in #{seconds} seconds with exit status ")
293
- if exit_status == 0
294
- puts Style.green("#{exit_status} (#{exit_status == 0 ? 'success' : 'failure'})")
295
- else
296
- puts Style.red("#{exit_status} (#{exit_status == 0 ? 'success' : 'failure'})")
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 = nil, &block)
262
- @interaction_handler.with_mapping(input_mapping, sudo_password, &block)
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
- # if app.info?
271
- # SSHKit.config.use_format :pretty
272
- # SSHKit.config.output_verbosity = :info
273
- # elsif app.debug?
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
@@ -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: :debug, strip: true, raise_on_non_zero_exit: false }.merge(args.extract_options!)
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)
@@ -1,3 +1,3 @@
1
1
  module OpsWalrus
2
- VERSION = "1.0.40"
2
+ VERSION = "1.0.43"
3
3
  end
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.40
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-07 00:00:00.000000000 Z
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