opswalrus 1.0.6 → 1.0.8

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: c6a371b7cd41b84c0ea93f89180204ea580b2a8d20c9d8686639663e6bf31379
4
- data.tar.gz: b27a653685068ba101cb89990a3fcd74b4eab39337f9592f35b9cc41f0014a48
3
+ metadata.gz: 5cc78e2afa6b27ff1d9c88d43b8ddf5e805768c020b22a31d8c51b592fedbb9c
4
+ data.tar.gz: 3f66378e5cfeb379aaed583ec180c14ecd5845bce81cf3e210fcac9bbe90b665
5
5
  SHA512:
6
- metadata.gz: 5928f1f07bc4ddd13f630c3967a73a196ec21999cbc1cf5d30247bd24dbec940c7fe207ff37520158b787ef635a70b41add3c6f3614d1000881ddda509361f3d
7
- data.tar.gz: e89db2ddc7e4b97dfcd2df4e01d2a525e0a5a8757283d1de2f136c8dd3fa64a4f04aa6daaf1efe81d812c622dc2572df996b19aa66dcb9cf664826b4c38a82b9
6
+ metadata.gz: a0f2aed13949ccafd89f6ae5ab274db40e0c9904c7f1eee1a15e19c22eefc5f24a5d47a62df04bd58628eca6c4a003a0063de57d796f48ee28e14156be794a70
7
+ data.tar.gz: ca70e69f2bba12706f983ec6ff78a1ab5a32e65c495bcb8e07b3f4cdccfac50ec3073b60085f7c42c885ec71e5d8bc8f369b0d51f6e86242c3efd4c9b68d1309
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.8)
5
5
  bcrypt_pbkdf (~> 1.1)
6
6
  citrus (~> 3.0)
7
7
  ed25519 (~> 1.3)
data/build.ops CHANGED
@@ -3,10 +3,26 @@ params:
3
3
 
4
4
  imports:
5
5
  core: "https://github.com/opswalrus/core.git"
6
+
6
7
  ...
7
8
 
9
+ # when you run this script, it should do something like:
10
+ # ~/sync/projects/ops/opswalrus on  main via 💎 v3.2.2
11
+ # ❯ ../ops.sh run build.ops version:1.0.7
12
+ # Write version.rb for version 1.0.7
13
+ # [localhost] Build gem: gem build opswalrus.gemspec
14
+ # [localhost] Check whether Bitwarden is locked or not: bw status
15
+ # [localhost] Get Rubygems OTP: bw get totp Rubygems
16
+ # [localhost] Push gem: gem push opswalrus-1.0.7.gem
17
+ # [localhost] Build docker image: docker build -t opswalrus/ops:1.0.7 .
18
+
19
+ # ~/sync/projects/ops/opswalrus on  main via 💎 v3.2.2 took 44s
20
+
21
+
8
22
  version = params.version
9
23
 
24
+ exit 1, "version parameter must be specified" unless version
25
+
10
26
  template = <<TEMPLATE
11
27
  module OpsWalrus
12
28
  VERSION = "{{ version }}"
@@ -17,4 +33,22 @@ puts "Write version.rb for version #{version}"
17
33
  core.template.write(path: "./lib/opswalrus/version.rb", template: template, variables: {version: version})
18
34
 
19
35
  sh("Build gem") { 'gem build opswalrus.gemspec' }
36
+ bw_status_output = sh("Check whether Bitwarden is locked or not") { 'bw status' }
37
+ # the status command currently exhibits an error in which it emits 'mac failed.' some number of times, so we need to filter that out
38
+ # see:
39
+ # - https://community.bitwarden.com/t/what-does-mac-failed-mean-exactly/29208
40
+ # - https://github.com/bitwarden/cli/issues/88
41
+ # - https://github.com/vwxyzjn/portwarden/issues/22
42
+ # ❯ bw status
43
+ # mac failed.
44
+ # {"serverUrl":"...","lastSync":"2023-08-17T19:14:09.384Z","userEmail":"...","userId":"...","status":"locked"}
45
+ bw_status_output = bw_status_output.gsub('mac failed.', '').strip
46
+ bw_status_json = bw_status_output.parse_json
47
+
48
+ if bw_status_json['status'] != 'unlocked'
49
+ exit 0, "Bitwarden is not unlocked. Please unlock bitwarden with: bw unlock"
50
+ end
51
+
52
+ totp = sh("Get Rubygems OTP") { 'bw get totp Rubygems' }
53
+ sh("Push gem", input: {"You have enabled multi-factor authentication. Please enter OTP code." => "#{totp}\n"}) { 'gem push opswalrus-{{ version }}.gem' }
20
54
  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
 
@@ -0,0 +1,21 @@
1
+ require 'json'
2
+
3
+ class String
4
+ def escape_single_quotes
5
+ gsub("'"){"\\'"}
6
+ end
7
+
8
+ def to_pathname
9
+ Pathname.new(self)
10
+ end
11
+
12
+ def parse_json
13
+ JSON.parse(self)
14
+ end
15
+ end
16
+
17
+ class Pathname
18
+ def to_pathname
19
+ self
20
+ end
21
+ end
@@ -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.8"
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.8
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
@@ -141,6 +141,7 @@ files:
141
141
  - lib/opswalrus/ops_file.rb
142
142
  - lib/opswalrus/ops_file_script.rb
143
143
  - lib/opswalrus/package_file.rb
144
+ - lib/opswalrus/patches.rb
144
145
  - lib/opswalrus/runtime_environment.rb
145
146
  - lib/opswalrus/sshkit_ext.rb
146
147
  - lib/opswalrus/traversable.rb