opswalrus 1.0.6 → 1.0.7

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c6a371b7cd41b84c0ea93f89180204ea580b2a8d20c9d8686639663e6bf31379
4
- data.tar.gz: b27a653685068ba101cb89990a3fcd74b4eab39337f9592f35b9cc41f0014a48
3
+ metadata.gz: b0c1c00f88ff00a4f7eb735a634ea35e1e8818b44fffdccea7a2347bac78e199
4
+ data.tar.gz: fe54254076c753cfeaf26906fa0f7475bd11807c0ac70d4067da03653b0f774c
5
5
  SHA512:
6
- metadata.gz: 5928f1f07bc4ddd13f630c3967a73a196ec21999cbc1cf5d30247bd24dbec940c7fe207ff37520158b787ef635a70b41add3c6f3614d1000881ddda509361f3d
7
- data.tar.gz: e89db2ddc7e4b97dfcd2df4e01d2a525e0a5a8757283d1de2f136c8dd3fa64a4f04aa6daaf1efe81d812c622dc2572df996b19aa66dcb9cf664826b4c38a82b9
6
+ metadata.gz: 28244a51a465941731663e9db3901049d36d1fa385a16b4c5ed9254c31fa62ca44012c2d776ef5b6d1d06906c012802b1352e166d72d28b3ec0aa6f9e27073f5
7
+ data.tar.gz: 39ccb70ca9dc5b2ecf67b1ab29adb7ccc79cb43ad57d256be0deb68b49909bc4c5cd4ddc2871e68aeeb374ed3d0202c365094536e7933f61c3ecf700a2178076
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- opswalrus (1.0.6)
4
+ opswalrus (1.0.7)
5
5
  bcrypt_pbkdf (~> 1.1)
6
6
  citrus (~> 3.0)
7
7
  ed25519 (~> 1.3)
data/build.ops CHANGED
@@ -7,6 +7,8 @@ imports:
7
7
 
8
8
  version = params.version
9
9
 
10
+ exit 1, "version parameter must be specified" unless version
11
+
10
12
  template = <<TEMPLATE
11
13
  module OpsWalrus
12
14
  VERSION = "{{ version }}"
@@ -17,4 +19,22 @@ puts "Write version.rb for version #{version}"
17
19
  core.template.write(path: "./lib/opswalrus/version.rb", template: template, variables: {version: version})
18
20
 
19
21
  sh("Build gem") { 'gem build opswalrus.gemspec' }
22
+ bw_status_output = sh("Check whether Bitwarden is locked or not") { 'bw status' }
23
+ # the status command currently exhibits an error in which it emits 'mac failed.' some number of times, so we need to filter that out
24
+ # see:
25
+ # - https://community.bitwarden.com/t/what-does-mac-failed-mean-exactly/29208
26
+ # - https://github.com/bitwarden/cli/issues/88
27
+ # - https://github.com/vwxyzjn/portwarden/issues/22
28
+ # ❯ bw status
29
+ # mac failed.
30
+ # {"serverUrl":"...","lastSync":"2023-08-17T19:14:09.384Z","userEmail":"...","userId":"...","status":"locked"}
31
+ bw_status_output = bw_status_output.gsub('mac failed.', '').strip
32
+ bw_status_json = bw_status_output.parse_json
33
+
34
+ if bw_status_json['status'] != 'unlocked'
35
+ exit 0, "Bitwarden is not unlocked. Please unlock bitwarden with: bw unlock"
36
+ end
37
+
38
+ totp = sh("Get Rubygems OTP") { 'bw get totp Rubygems' }
39
+ sh("Push gem", input: {"You have enabled multi-factor authentication. Please enter OTP code." => "#{totp}\n"}) { 'gem push opswalrus-{{ version }}.gem' }
20
40
  sh("Build docker image") { 'docker build -t opswalrus/ops:{{ version }} .' }
data/lib/opswalrus/app.rb CHANGED
@@ -7,6 +7,7 @@ require "socket"
7
7
  require "stringio"
8
8
  require "yaml"
9
9
  require "pathname"
10
+ require_relative "patches"
10
11
  require_relative "git"
11
12
  require_relative "host"
12
13
  require_relative "hosts_file"
@@ -14,21 +15,6 @@ require_relative "operation_runner"
14
15
  require_relative "bundler"
15
16
  require_relative "package_file"
16
17
 
17
- class String
18
- def escape_single_quotes
19
- gsub("'"){"\\'"}
20
- end
21
-
22
- def to_pathname
23
- Pathname.new(self)
24
- end
25
- end
26
-
27
- class Pathname
28
- def to_pathname
29
- self
30
- end
31
- end
32
18
 
33
19
  module OpsWalrus
34
20
  class Error < StandardError
@@ -131,6 +117,7 @@ module OpsWalrus
131
117
  def prompt_sudo_password
132
118
  password = IO::console.getpass(LOCAL_SUDO_PASSWORD_PROMPT)
133
119
  set_sudo_password(password)
120
+ # puts "sudo password = |#{password}|"
134
121
  nil
135
122
  end
136
123
 
@@ -7,18 +7,18 @@ module OpsWalrus
7
7
 
8
8
  module HostDSL
9
9
  # returns the stdout from the command
10
- def sh(desc_or_cmd = nil, cmd = nil, stdin: nil, &block)
11
- out, err, status = *shell!(desc_or_cmd, cmd, block, stdin: stdin)
10
+ def sh(desc_or_cmd = nil, cmd = nil, input: nil, &block)
11
+ out, err, status = *shell!(desc_or_cmd, cmd, block, input: input)
12
12
  out
13
13
  end
14
14
 
15
15
  # returns the tuple: [stdout, stderr, exit_status]
16
- def shell(desc_or_cmd = nil, cmd = nil, stdin: nil, &block)
17
- shell!(desc_or_cmd, cmd, block, stdin: stdin)
16
+ def shell(desc_or_cmd = nil, cmd = nil, input: nil, &block)
17
+ shell!(desc_or_cmd, cmd, block, input: input)
18
18
  end
19
19
 
20
20
  # returns the tuple: [stdout, stderr, exit_status]
21
- def shell!(desc_or_cmd = nil, cmd = nil, block = nil, stdin: nil)
21
+ def shell!(desc_or_cmd = nil, cmd = nil, block = nil, input: nil)
22
22
  # description = nil
23
23
 
24
24
  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
@@ -45,7 +45,7 @@ module OpsWalrus
45
45
  # puts "shell: #{cmd.inspect}"
46
46
  # puts "sudo_password: #{sudo_password}"
47
47
 
48
- sshkit_cmd = execute_cmd(cmd)
48
+ sshkit_cmd = execute_cmd(cmd, input: input)
49
49
 
50
50
  [sshkit_cmd.full_stdout, sshkit_cmd.full_stderr, sshkit_cmd.exit_status]
51
51
  end
@@ -203,6 +203,10 @@ module OpsWalrus
203
203
  })
204
204
  end
205
205
 
206
+ def set_runtime_env(runtime_env)
207
+ @runtime_env = runtime_env
208
+ end
209
+
206
210
  def set_ssh_session_connection(sshkit_backend)
207
211
  @sshkit_backend = sshkit_backend
208
212
  end
@@ -212,18 +216,27 @@ module OpsWalrus
212
216
  end
213
217
 
214
218
  def clear_ssh_session
219
+ @runtime_env = nil
215
220
  @sshkit_backend = nil
216
221
  @tmp_bundle_root_dir = nil
217
222
  end
218
223
 
219
- def execute(*args)
224
+ def execute(*args, input: nil)
220
225
  # puts "interaction handler responds with: #{ssh_password}"
221
- @sshkit_backend.capture(*args, interaction_handler: SudoPasswordMapper.new(ssh_password).interaction_handler, verbosity: :info)
226
+ # @sshkit_backend.capture(*args, interaction_handler: SudoPasswordMapper.new(ssh_password).interaction_handler, verbosity: :info)
222
227
  # @sshkit_backend.capture(*args, interaction_handler: SudoPromptInteractionHandler.new, verbosity: :info)
228
+
229
+ @runtime_env.handle_input(input, ssh_password) do |interaction_handler|
230
+ @sshkit_backend.capture(*args, interaction_handler: interaction_handler, verbosity: :info)
231
+ end
232
+
223
233
  end
224
234
 
225
- def execute_cmd(*args)
226
- @sshkit_backend.execute_cmd(*args, interaction_handler: SudoPasswordMapper.new(ssh_password).interaction_handler, verbosity: :info)
235
+ def execute_cmd(*args, input: nil)
236
+ # @sshkit_backend.execute_cmd(*args, interaction_handler: SudoPasswordMapper.new(ssh_password).interaction_handler, verbosity: :info)
237
+ @runtime_env.handle_input(input, ssh_password) do |interaction_handler|
238
+ @sshkit_backend.execute_cmd(*args, interaction_handler: interaction_handler, verbosity: :info)
239
+ end
227
240
  end
228
241
 
229
242
  def upload(local_path_or_io, remote_path)
@@ -2,6 +2,91 @@ require 'sshkit'
2
2
 
3
3
  module OpsWalrus
4
4
 
5
+ class ScopedMappingInteractionHandler
6
+ attr_accessor :input_mappings # Hash[ String | Regex => String ]
7
+
8
+ def initialize(mapping, log_level = nil)
9
+ @log_level = log_level
10
+ @input_mappings = mapping
11
+ end
12
+
13
+ # temporarily adds a sudo password mapping to the interaction handler while the given block is being evaluated
14
+ # when the given block returns, then the temporary mapping is removed from the interaction handler
15
+ # def with_sudo_password(password, &block)
16
+ # with_mapping({
17
+ # /\[sudo\] password for .*?:\s*/ => "#{password}\n",
18
+ # App::LOCAL_SUDO_PASSWORD_PROMPT => "#{password}\n",
19
+ # # /\s+/ => nil, # unnecessary
20
+ # }, &block)
21
+ # end
22
+
23
+ # sudo_password : String
24
+ def mapping_for_sudo_password(sudo_password)
25
+ {
26
+ /\[sudo\] password for .*?:\s*/ => "#{sudo_password}\n",
27
+ App::LOCAL_SUDO_PASSWORD_PROMPT => "#{sudo_password}\n",
28
+ # /\s+/ => nil, # unnecessary
29
+ }
30
+ end
31
+
32
+ # temporarily adds the specified input mapping to the interaction handler while the given block is being evaluated
33
+ # when the given block returns, then the temporary mapping is removed from the interaction handler
34
+ #
35
+ # mapping : Hash[ String | Regex => String ]
36
+ def with_mapping(mapping, sudo_password = nil)
37
+ mapping ||= {}
38
+
39
+ raise ArgumentError.new("mapping must be a Hash") unless mapping.is_a?(Hash)
40
+
41
+ if sudo_password
42
+ mapping.merge!(mapping_for_sudo_password(sudo_password))
43
+ end
44
+
45
+ if mapping.empty?
46
+ yield self
47
+ else
48
+ yield ScopedMappingInteractionHandler.new(@input_mappings.merge(mapping), @log_level)
49
+ end
50
+ end
51
+
52
+ # adds the specified input mapping to the interaction handler
53
+ #
54
+ # mapping : Hash[ String | Regex => String ]
55
+ def add_mapping(mapping)
56
+ @input_mappings.merge!(mapping)
57
+ end
58
+
59
+ def on_data(_command, stream_name, data, channel)
60
+ log("Looking up response for #{stream_name} message #{data.inspect}")
61
+
62
+ response_data = begin
63
+ first_matching_key_value_pair = @input_mappings.find {|k, _v| k === data }
64
+ first_matching_key_value_pair&.last
65
+ end
66
+
67
+ if response_data.nil?
68
+ log("Unable to find interaction handler mapping for #{stream_name}: #{data.inspect} so no response was sent")
69
+ else
70
+ log("Sending #{response_data.inspect}")
71
+ if channel.respond_to?(:send_data) # Net SSH Channel
72
+ channel.send_data(response_data)
73
+ elsif channel.respond_to?(:write) # Local IO
74
+ channel.write(response_data)
75
+ else
76
+ raise "Unable to write response data to channel #{channel.inspect} - does not support '#send_data' or '#write'"
77
+ end
78
+ end
79
+ end
80
+
81
+ private
82
+
83
+ def log(message)
84
+ # puts message
85
+ SSHKit.config.output.send(@log_level, message) unless @log_level.nil?
86
+ end
87
+
88
+ end
89
+
5
90
  class PasswdInteractionHandler
6
91
  def on_data(command, stream_name, data, channel)
7
92
  # puts data
@@ -16,12 +16,16 @@ module SSHKit
16
16
  cmd.started = Time.now
17
17
  PTY.spawn(cmd.to_command) do |stdout, stdin, pid|
18
18
  stdout_thread = Thread.new do
19
+ # debug_log = StringIO.new
19
20
  buffer = ""
20
21
  partial_buffer = ""
21
22
  while !stdout.closed?
23
+ # debug_log.puts "!!!\nbuffer=#{buffer}|EOL|\npartial=#{partial_buffer}|EOL|"
22
24
  # puts "9" * 80
23
25
  begin
24
- stdout.read_nonblock(4096, partial_buffer)
26
+ # partial_buffer = ""
27
+ # stdout.read_nonblock(4096, partial_buffer)
28
+ partial_buffer = stdout.read_nonblock(4096)
25
29
  buffer << partial_buffer
26
30
  # puts "nonblocking1. buffer=#{buffer} partial_buffer=#{partial_buffer}"
27
31
  buffer = handle_data_for_stdout(output, cmd, buffer, stdin, false)
@@ -48,6 +52,10 @@ module SSHKit
48
52
  end
49
53
  end
50
54
  # puts "end!"
55
+ # debug_log.puts "!!!\nbuffer=#{buffer}|EOL|\npartial=#{partial_buffer}|EOL|"
56
+
57
+ # puts "*" * 80
58
+ # puts debug_log.string
51
59
 
52
60
  end
53
61
  stdout_thread.join
@@ -59,12 +67,14 @@ module SSHKit
59
67
 
60
68
  # returns [complete lines, new buffer]
61
69
  def split_buffer(buffer)
62
- lines = buffer.split(/(\r\n)\r|\n/)
70
+ lines = buffer.split(/(\r\n)|\r|\n/)
63
71
  buffer = lines.pop
64
72
  [lines, buffer]
65
73
  end
66
74
 
67
75
  def handle_data_for_stdout(output, cmd, buffer, stdin, is_blocked)
76
+ # puts "handling data for stdout: #{buffer}"
77
+
68
78
  # we're blocked on reading, so let's process the buffer
69
79
  lines, buffer = split_buffer(buffer)
70
80
  lines.each do |line|
@@ -121,13 +121,15 @@ module OpsWalrus
121
121
  # bootstrap_shell_script = BootstrapLinuxHostShellScript
122
122
  # on sshkit_hosts do |sshkit_host|
123
123
  SSHKit::Coordinator.new(sshkit_hosts).each(in: kwargs[:in] || :parallel) do |sshkit_host|
124
- host = sshkit_host_to_ops_host_map[sshkit_host]
125
124
 
125
+ # in this context, self is an instance of one of the subclasses of SSHKit::Backend::Abstract, e.g. SSHKit::Backend::Netssh
126
+
127
+ host = sshkit_host_to_ops_host_map[sshkit_host]
126
128
  # puts "#{host.alias} / #{host}:"
127
129
 
128
130
  begin
129
- # in this context, self is an instance of one of the subclasses of SSHKit::Backend::Abstract, e.g. SSHKit::Backend::Netssh
130
- host.set_ssh_session_connection(self)
131
+ host.set_runtime_env(runtime_env)
132
+ host.set_ssh_session_connection(self) # self is an instance of one of the subclasses of SSHKit::Backend::Abstract, e.g. SSHKit::Backend::Netssh
131
133
 
132
134
  # copy over bootstrap shell script
133
135
  # io = StringIO.new(bootstrap_shell_script)
@@ -199,11 +201,14 @@ module OpsWalrus
199
201
  @runtime_env.app.inventory(tags)
200
202
  end
201
203
 
202
- def exit(exit_status)
204
+ def exit(exit_status, message = nil)
205
+ if message
206
+ puts message
207
+ end
203
208
  result = if exit_status == 0
204
- Success.new(nil)
209
+ Invocation::Success.new(nil)
205
210
  else
206
- Error.new(nil, exit_status)
211
+ Invocation::Error.new(nil, exit_status)
207
212
  end
208
213
  throw :exit_now, result
209
214
  end
@@ -240,18 +245,18 @@ module OpsWalrus
240
245
  end
241
246
 
242
247
  # returns the stdout from the command
243
- def sh(desc_or_cmd = nil, cmd = nil, stdin: nil, &block)
244
- out, err, status = *shell!(desc_or_cmd, cmd, block, stdin: stdin)
248
+ def sh(desc_or_cmd = nil, cmd = nil, input: nil, &block)
249
+ out, err, status = *shell!(desc_or_cmd, cmd, block, input: input)
245
250
  out
246
251
  end
247
252
 
248
253
  # returns the tuple: [stdout, stderr, exit_status]
249
- def shell(desc_or_cmd = nil, cmd = nil, stdin: nil, &block)
250
- shell!(desc_or_cmd, cmd, block, stdin: stdin)
254
+ def shell(desc_or_cmd = nil, cmd = nil, input: nil, &block)
255
+ shell!(desc_or_cmd, cmd, block, input: input)
251
256
  end
252
257
 
253
258
  # returns the tuple: [stdout, stderr, exit_status]
254
- def shell!(desc_or_cmd = nil, cmd = nil, block = nil, stdin: nil)
259
+ def shell!(desc_or_cmd = nil, cmd = nil, block = nil, input: nil)
255
260
  # description = nil
256
261
 
257
262
  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
@@ -272,8 +277,8 @@ module OpsWalrus
272
277
 
273
278
  return unless cmd && !cmd.strip.empty?
274
279
 
275
- sudo_password = @runtime_env.sudo_password
276
- sudo_password &&= sudo_password.gsub(/\n+$/,'') # remove trailing newlines from sudo_password
280
+ # sudo_password = @runtime_env.sudo_password
281
+ # sudo_password &&= sudo_password.gsub(/\n+$/,'') # remove trailing newlines from sudo_password
277
282
 
278
283
  # puts "shell: #{cmd}"
279
284
  # puts "shell: #{cmd.inspect}"
@@ -281,10 +286,14 @@ module OpsWalrus
281
286
 
282
287
  # sshkit_cmd = SSHKit::Backend::LocalNonBlocking.new {
283
288
  # sshkit_cmd = SSHKit::Backend::LocalPty.new {
284
- sshkit_cmd = backend.execute_cmd(cmd, interaction_handler: SudoPasswordMapper.new(sudo_password).interaction_handler, verbosity: :info)
289
+ # sshkit_cmd = backend.execute_cmd(cmd, interaction_handler: SudoPasswordMapper.new(sudo_password).interaction_handler, verbosity: :info)
285
290
  # execute_cmd(cmd, interaction_handler: SudoPromptInteractionHandler.new, verbosity: :info)
286
291
  # }.run
287
292
 
293
+ sshkit_cmd = @runtime_env.handle_input(input) do |interaction_handler|
294
+ backend.execute_cmd(cmd, interaction_handler: interaction_handler, verbosity: :info)
295
+ end
296
+
288
297
  [sshkit_cmd.full_stdout, sshkit_cmd.full_stderr, sshkit_cmd.exit_status]
289
298
  end
290
299
 
@@ -3,6 +3,7 @@ require 'shellwords'
3
3
  require 'socket'
4
4
  require 'sshkit'
5
5
 
6
+ require_relative 'interaction_handlers'
6
7
  require_relative 'traversable'
7
8
  require_relative 'walrus_lang'
8
9
 
@@ -196,9 +197,27 @@ module OpsWalrus
196
197
  @bundle_load_path = LoadPath.new(self, @app.bundle_dir)
197
198
  @app_load_path = LoadPath.new(self, @app.pwd)
198
199
 
200
+ @interaction_handler = ScopedMappingInteractionHandler.new({
201
+ /\[sudo\] password for .*?:\s*/ => "#{sudo_password}\n",
202
+ })
203
+
199
204
  configure_sshkit
200
205
  end
201
206
 
207
+ # def with_sudo_password(password, &block)
208
+ # @interaction_handler.with_sudo_password(password, &block)
209
+ # end
210
+
211
+ # input_mapping : Hash[ String | Regex => String ]
212
+ # sudo_password : String
213
+ def handle_input(input_mapping, sudo_password = nil, &block)
214
+ @interaction_handler.with_mapping(input_mapping, sudo_password, &block)
215
+ end
216
+
217
+ # def handle_input_with_sudo_password(input_mapping, password, &block)
218
+ # @interaction_handler.with_mapping(input_mapping, password, &block)
219
+ # end
220
+
202
221
  # configure sshkit globally
203
222
  def configure_sshkit
204
223
  SSHKit.config.use_format :blackhole
@@ -1,3 +1,3 @@
1
1
  module OpsWalrus
2
- VERSION = "1.0.6"
2
+ VERSION = "1.0.7"
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.6
4
+ version: 1.0.7
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-08-17 00:00:00.000000000 Z
11
+ date: 2023-08-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: citrus