opswalrus 1.0.51 → 1.0.53

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: be0988f270b372d5ad10f8ed1abb90db64ce6d07c34a1f17854d24e397c80d67
4
- data.tar.gz: bdf5f694430d8178f3a4799d53cad884fc3a8adaecc74c14bf76b2a1a451a756
3
+ metadata.gz: 007f1ac465143fdd629539509b94a8bf7230b034817526d1265768f97eae0786
4
+ data.tar.gz: 9d0cc607ce7c9fcd06d9e402254b3eb5a75a68687a1ee485aa915b3cd488e2b6
5
5
  SHA512:
6
- metadata.gz: 54cf7193022eb5fec71c71a4417668567e5134a7fcb5f72a31ed10ecee7d52765c3fbe5ecb33ee70052d5f5c3e17f60076352beec4a16397944493ccd25cf317
7
- data.tar.gz: 293e8f8948d3c0d7ec7f64ea40551519fe5f14be4317185603f49e33e74f5ec188e1121933b51c1234a261fbd56e18a2bfa6ecc78d74e313d4c996763715edf4
6
+ metadata.gz: 83b67721de074efa3ad368ed21ec469d40928351c4ccc6f32d530e3bbda2cdd81b62f2b72692a4796b74d1efb7bbbfa31721db0eefb93864069dc277f8aa66fe
7
+ data.tar.gz: b54d628df099de1f4c2eea591b86449b5fd254d495ccae38f89dd95f5d1137171aaf9e6c1bd2023613f1c33561b5e49bd12d5696d6bc197bc065ead0a79aed1d
data/Gemfile CHANGED
@@ -7,3 +7,5 @@ gemspec
7
7
 
8
8
  gem "rake", "~> 13.0"
9
9
  gem "rspec", "~> 3.0"
10
+
11
+ gem 'solargraph', group: :development
data/Gemfile.lock CHANGED
@@ -1,7 +1,8 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- opswalrus (1.0.51)
4
+ opswalrus (1.0.53)
5
+ activesupport (~> 7.0)
5
6
  bcrypt_pbkdf (~> 1.1)
6
7
  binding_of_caller (~> 1.0)
7
8
  citrus (~> 3.0)
@@ -17,28 +18,61 @@ PATH
17
18
  GEM
18
19
  remote: https://rubygems.org/
19
20
  specs:
21
+ activesupport (7.0.8)
22
+ concurrent-ruby (~> 1.0, >= 1.0.2)
23
+ i18n (>= 1.6, < 2)
24
+ minitest (>= 5.1)
25
+ tzinfo (~> 2.0)
20
26
  addressable (2.8.5)
21
27
  public_suffix (>= 2.0.2, < 6.0)
28
+ ast (2.4.2)
29
+ backport (1.2.0)
30
+ base64 (0.1.1)
22
31
  bcrypt_pbkdf (1.1.0)
32
+ benchmark (0.2.1)
23
33
  binding_of_caller (1.0.0)
24
34
  debug_inspector (>= 0.0.1)
25
35
  citrus (3.0.2)
26
36
  concurrent-ruby (1.2.2)
27
37
  debug_inspector (1.1.0)
28
38
  diff-lcs (1.5.0)
39
+ e2mmap (0.1.0)
29
40
  ed25519 (1.3.0)
30
41
  git (1.18.0)
31
42
  addressable (~> 2.8)
32
43
  rchardet (~> 1.8)
33
44
  gli (2.21.1)
45
+ i18n (1.14.1)
46
+ concurrent-ruby (~> 1.0)
47
+ jaro_winkler (1.5.6)
48
+ json (2.6.3)
49
+ kramdown (2.4.0)
50
+ rexml
51
+ kramdown-parser-gfm (1.1.0)
52
+ kramdown (~> 2.0)
53
+ language_server-protocol (3.17.0.3)
54
+ minitest (5.20.0)
34
55
  net-scp (4.0.0)
35
56
  net-ssh (>= 2.6.5, < 8.0.0)
36
57
  net-ssh (7.2.0)
58
+ nokogiri (1.15.4-x86_64-linux)
59
+ racc (~> 1.4)
60
+ parallel (1.23.0)
61
+ parser (3.2.2.3)
62
+ ast (~> 2.4.1)
63
+ racc
37
64
  pastel (0.8.0)
38
65
  tty-color (~> 0.5)
39
66
  public_suffix (5.0.3)
67
+ racc (1.7.1)
68
+ rainbow (3.1.1)
40
69
  rake (13.0.6)
70
+ rbs (2.8.4)
41
71
  rchardet (1.8.0)
72
+ regexp_parser (2.8.1)
73
+ reverse_markdown (2.1.1)
74
+ nokogiri
75
+ rexml (3.2.6)
42
76
  rspec (3.12.0)
43
77
  rspec-core (~> 3.12.0)
44
78
  rspec-expectations (~> 3.12.0)
@@ -52,12 +86,45 @@ GEM
52
86
  diff-lcs (>= 1.2.0, < 2.0)
53
87
  rspec-support (~> 3.12.0)
54
88
  rspec-support (3.12.1)
89
+ rubocop (1.56.3)
90
+ base64 (~> 0.1.1)
91
+ json (~> 2.3)
92
+ language_server-protocol (>= 3.17.0)
93
+ parallel (~> 1.10)
94
+ parser (>= 3.2.2.3)
95
+ rainbow (>= 2.2.2, < 4.0)
96
+ regexp_parser (>= 1.8, < 3.0)
97
+ rexml (>= 3.2.5, < 4.0)
98
+ rubocop-ast (>= 1.28.1, < 2.0)
99
+ ruby-progressbar (~> 1.7)
100
+ unicode-display_width (>= 2.4.0, < 3.0)
101
+ rubocop-ast (1.29.0)
102
+ parser (>= 3.2.1.0)
103
+ ruby-progressbar (1.13.0)
55
104
  rubyzip (2.3.2)
56
105
  semantic_logger (4.14.0)
57
106
  concurrent-ruby (~> 1.0)
107
+ solargraph (0.49.0)
108
+ backport (~> 1.2)
109
+ benchmark
110
+ bundler (~> 2.0)
111
+ diff-lcs (~> 1.4)
112
+ e2mmap
113
+ jaro_winkler (~> 1.5)
114
+ kramdown (~> 2.3)
115
+ kramdown-parser-gfm (~> 1.1)
116
+ parser (~> 3.0)
117
+ rbs (~> 2.0)
118
+ reverse_markdown (~> 2.0)
119
+ rubocop (~> 1.38)
120
+ thor (~> 1.0)
121
+ tilt (~> 2.0)
122
+ yard (~> 0.9, >= 0.9.24)
58
123
  sshkit (1.21.5)
59
124
  net-scp (>= 1.1.2)
60
125
  net-ssh (>= 2.8.0)
126
+ thor (1.2.2)
127
+ tilt (2.3.0)
61
128
  tty-color (0.6.0)
62
129
  tty-cursor (0.7.1)
63
130
  tty-editor (0.7.0)
@@ -70,7 +137,11 @@ GEM
70
137
  tty-screen (~> 0.8)
71
138
  wisper (~> 2.0)
72
139
  tty-screen (0.8.1)
140
+ tzinfo (2.0.6)
141
+ concurrent-ruby (~> 1.0)
142
+ unicode-display_width (2.4.2)
73
143
  wisper (2.0.1)
144
+ yard (0.9.34)
74
145
 
75
146
  PLATFORMS
76
147
  x86_64-linux
@@ -79,6 +150,7 @@ DEPENDENCIES
79
150
  opswalrus!
80
151
  rake (~> 13.0)
81
152
  rspec (~> 3.0)
153
+ solargraph
82
154
 
83
155
  BUNDLED WITH
84
156
  2.4.10
@@ -0,0 +1,64 @@
1
+ params:
2
+ delay: integer? # default: 1 - 1 second delay before reboot
3
+ sync: boolean? # default: true - wait for the remote host to become available again before returning success/failure
4
+ timeout: integer? # default: 300 - 300 seconds (5 minutes)
5
+ ...
6
+
7
+ delay = params.delay.integer!(default: 1)
8
+ sync = params.sync.boolean!(default: true)
9
+ timeout = params.timeout.integer!(default: 300)
10
+
11
+ delay = 1 if delay < 1
12
+
13
+ ssh_noprep in: :sequence do
14
+ # ssh_noprep do
15
+
16
+ # survey of command options:
17
+ # sudo reboot
18
+ # sudo systemctl reboot
19
+
20
+ # desc "Rebooting #{to_s} (alias=#{self.alias})"
21
+ # reboot_success = sh? 'sudo /bin/sh -c "(sleep {{ delay }} && reboot) &"'.mustache
22
+ # puts reboot_success
23
+
24
+ # reconnect_time = nil
25
+ # reconnect_success = if sync
26
+ # desc "Waiting for #{to_s} (alias=#{self.alias}) to finish rebooting"
27
+ # initial_reconnect_delay = delay + 10
28
+ # sleep initial_reconnect_delay
29
+
30
+ # reconnected = false
31
+ # give_up = false
32
+ # t1 = Time.now
33
+ # until reconnected || give_up
34
+ # begin
35
+ # reconnected = sh?('true')
36
+ # # while trying to reconnect, we expect the following exceptions:
37
+ # # 1. Net::SSH::Disconnect < Net::SSH::Exception with message: "connection closed by remote host"
38
+ # # 2. Errno::ECONNRESET < SystemCallError with message: "Connection reset by peer"
39
+ # rescue Net::SSH::Disconnect, Errno::ECONNRESET => e
40
+ # # noop; we expect these while we're trying to reconnect
41
+ # rescue => e
42
+ # puts "#{e.class} < #{e.class.superclass}"
43
+ # puts e.message
44
+ # puts e.backtrace.take(5).join("\n")
45
+ # end
46
+
47
+ # wait_time_elapsed_in_seconds = Time.now - t1
48
+ # give_up = wait_time_elapsed_in_seconds > timeout
49
+ # sleep 5
50
+ # end
51
+ # reconnect_time = initial_reconnect_delay + (Time.now - t1)
52
+ # reconnected
53
+ # else
54
+ # false
55
+ # end
56
+
57
+ # {
58
+ # success: reboot_success && (sync == reconnect_success),
59
+ # rebooted: reboot_success,
60
+ # reconnected: reconnect_success,
61
+ # reboot_duration: reconnect_time
62
+ # }
63
+ reboot(delay: delay, sync: sync, timeout: timeout)
64
+ end
@@ -0,0 +1,12 @@
1
+ params:
2
+ ops_file: OpsFile
3
+ operation_kv_args: array string
4
+ ...
5
+ ssh in: :sequence do
6
+ # ssh_noprep do
7
+ puts params.stringify_keys!
8
+ desc "Running `#{params.ops_file.ops_file_path} #{params.operation_kv_args.join(' ')}` on #{to_s} (alias=#{self.alias})"
9
+ # run_ops(ops_command, ops_command_options = nil, command_arguments, in_bundle_root_dir: true, ops_prompt_for_sudo_password: false)
10
+ puts self.inspect
11
+ self._invoke_remote(params.ops_file, *params.operation_kv_args)
12
+ end
data/lib/opswalrus/app.rb CHANGED
@@ -38,11 +38,11 @@ module OpsWalrus
38
38
  attr_reader :identity_file_paths
39
39
 
40
40
  def initialize(pwd = Dir.pwd)
41
- SemanticLogger.default_level = :info
41
+ SemanticLogger.default_level = :warn
42
42
  # SemanticLogger.add_appender(file_name: 'development.log', formatter: :color) # Log to a file, and use the colorized formatter
43
43
  SemanticLogger.add_appender(io: $stdout, formatter: :color) # Log errors and above to standard error:
44
44
  @logger = SemanticLogger[OpsWalrus] # Logger.new($stdout, level: Logger::INFO)
45
- @logger.level = :info # , :trace or 'trace'
45
+ @logger.level = :warn # :trace or 'trace'
46
46
 
47
47
  # @logger.warn Style.yellow("warn"), foo: "bar", baz: {qux: "quux"}
48
48
  # @logger.info Style.yellow("info"), foo: "bar", baz: {qux: "quux"}
@@ -216,18 +216,22 @@ module OpsWalrus
216
216
  end
217
217
 
218
218
  def bootstrap()
219
- set_pwd(__FILE__.to_pathname.dirname)
220
- bootstrap_ops_file = OpsFile.new(self, __FILE__.to_pathname.dirname.join("_bootstrap.ops"))
221
- op = OperationRunner.new(self, bootstrap_ops_file)
222
- op.run([], params_json_hash: @params)
219
+ run_internal("_bootstrap.ops")
223
220
  end
224
221
 
225
222
  def shell(command)
223
+ run_internal("_shell.ops", {"command" => command})
224
+ end
225
+
226
+ def reboot()
227
+ run_internal("_reboot.ops")
228
+ end
229
+
230
+ def run_internal(ops_file_name, params = @params)
226
231
  set_pwd(__FILE__.to_pathname.dirname)
227
- shell_ops_file = OpsFile.new(self, __FILE__.to_pathname.dirname.join("_shell.ops"))
228
- op = OperationRunner.new(self, shell_ops_file)
229
- puts "running #{command}"
230
- result = op.run([], params_json_hash: {"command" => command})
232
+ internal_ops_file = OpsFile.new(self, __FILE__.to_pathname.dirname.join(ops_file_name))
233
+ op = OperationRunner.new(self, internal_ops_file)
234
+ result = op.run([], params_json_hash: params)
231
235
  puts "result class=#{result.class}"
232
236
  exit_status = result.exit_status
233
237
  stdout = JSON.pretty_generate(result.value)
@@ -243,7 +247,45 @@ module OpsWalrus
243
247
  1
244
248
  end
245
249
 
246
- # args is of the form ["github.com/davidkellis/my-package/sub-package1", "operation1", "arg1:val1", "arg2:val2", "arg3:val3"]
250
+ # package_operation_and_args is of the form ["github.com/davidkellis/my-package/sub-package1", "operation1", "arg1:val1", "arg2:val2", "arg3:val3"]
251
+ # if the first argument is the path to a .ops file, then treat it as a local path, and add the containing package
252
+ # to the load path
253
+ # otherwise, copy the
254
+ # returns the exit status code that the script should terminate with
255
+ def run_remote(package_operation_and_args, update_bundle: false)
256
+ return 0 if package_operation_and_args.empty?
257
+
258
+ ops_file_path, operation_kv_args, tmp_bundle_root_dir = get_entry_point_ops_file_and_args(package_operation_and_args)
259
+
260
+ ops_file = load_entry_point_ops_file(ops_file_path, tmp_bundle_root_dir)
261
+
262
+ bundler.update if update_bundle
263
+
264
+ debug "Running: #{ops_file.ops_file_path}"
265
+
266
+ internal_ops_file = OpsFile.new(self, __FILE__.to_pathname.dirname.join("_run_remote.ops"))
267
+
268
+ op = OperationRunner.new(self, internal_ops_file)
269
+ result = op.run([], params_json_hash: {ops_file: ops_file, operation_kv_args: operation_kv_args})
270
+ exit_status = result.exit_status
271
+
272
+ debug "Op exit_status"
273
+ debug exit_status
274
+
275
+ debug "Op output"
276
+ debug JSON.pretty_generate(result.value)
277
+
278
+ puts JSON.pretty_generate(result.value)
279
+
280
+ exit_status
281
+ rescue Error => e
282
+ puts "Error: #{e.message}"
283
+ 1
284
+ ensure
285
+ FileUtils.remove_entry(tmp_bundle_root_dir) if tmp_bundle_root_dir
286
+ end
287
+
288
+ # package_operation_and_args is of the form ["github.com/davidkellis/my-package/sub-package1", "operation1", "arg1:val1", "arg2:val2", "arg3:val3"]
247
289
  # if the first argument is the path to a .ops file, then treat it as a local path, and add the containing package
248
290
  # to the load path
249
291
  # otherwise, copy the
data/lib/opswalrus/cli.rb CHANGED
@@ -30,13 +30,11 @@ module OpsWalrus
30
30
 
31
31
  program_desc 'ops is an operation runner'
32
32
 
33
- switch [:v, :verbose], desc: "Verbose output"
34
- switch :debug, desc: "Debug output"
35
- switch :trace, desc: "Trace output"
33
+ switch :loud, desc: "Verbose output"
34
+ switch :louder, desc: "Debug output"
35
+ switch :loudest, desc: "Trace output"
36
36
 
37
- switch :noop, desc: "Perform a dry run"
38
- switch :dryrun, desc: "Perform a dry run"
39
- switch :dry_run, desc: "Perform a dry run"
37
+ switch :dry, desc: "Perform a dry run"
40
38
 
41
39
  flag [:h, :hosts], multiple: true, desc: "Specify the hosts.yaml file"
42
40
  flag [:t, :tags], multiple: true, desc: "Specify a set of tags to filter the hosts by"
@@ -61,7 +59,7 @@ module OpsWalrus
61
59
  hosts = global_options[:hosts]
62
60
  tags = global_options[:tags]
63
61
 
64
- $app.set_log_level(global_options[:trace] && :trace || global_options[:debug] && :debug || global_options[:verbose] && :info || :warn)
62
+ $app.set_log_level(global_options[:loudest] && :trace || global_options[:louder] && :debug || global_options[:loud] && :info || :warn)
65
63
 
66
64
  $app.report_inventory(hosts, tags: tags)
67
65
  end
@@ -123,12 +121,10 @@ module OpsWalrus
123
121
  long_desc 'Bootstrap a set of hotss to run opswalrus: install dependencies, ruby, opswalrus gem'
124
122
  command :bootstrap do |c|
125
123
  # dry run
126
- c.switch :noop, desc: "Perform a dry run"
127
- c.switch :dryrun, desc: "Perform a dry run"
128
- c.switch :dry_run, desc: "Perform a dry run"
124
+ c.switch :dry, desc: "Perform a dry run"
129
125
 
130
126
  c.action do |global_options, options, args|
131
- $app.set_log_level(global_options[:trace] && :trace || global_options[:debug] && :debug || global_options[:verbose] && :info || :warn)
127
+ $app.set_log_level(global_options[:loudest] && :trace || global_options[:louder] && :debug || global_options[:loud] && :info || :warn)
132
128
 
133
129
  hosts = global_options[:hosts]
134
130
  $app.set_inventory_hosts(hosts)
@@ -141,7 +137,7 @@ module OpsWalrus
141
137
 
142
138
  $app.set_identity_files(id_files)
143
139
 
144
- dry_run = [:noop, :dryrun, :dry_run].any? {|sym| global_options[sym] || options[sym] }
140
+ dry_run = global_options[:dry] || options[:dry]
145
141
  $app.dry_run! if dry_run
146
142
 
147
143
  $app.bootstrap()
@@ -155,12 +151,10 @@ module OpsWalrus
155
151
  c.flag [:u, :user], desc: "Specify the user that the operation will run as"
156
152
 
157
153
  # dry run
158
- c.switch :noop, desc: "Perform a dry run"
159
- c.switch :dryrun, desc: "Perform a dry run"
160
- c.switch :dry_run, desc: "Perform a dry run"
154
+ c.switch :dry, desc: "Perform a dry run"
161
155
 
162
156
  c.action do |global_options, options, args|
163
- $app.set_log_level(global_options[:trace] && :trace || global_options[:debug] && :debug || global_options[:verbose] && :info || :warn)
157
+ $app.set_log_level(global_options[:loudest] && :trace || global_options[:louder] && :debug || global_options[:loud] && :info || :warn)
164
158
 
165
159
  hosts = global_options[:hosts]
166
160
  $app.set_inventory_hosts(hosts)
@@ -176,7 +170,7 @@ module OpsWalrus
176
170
 
177
171
  $app.set_identity_files(id_files)
178
172
 
179
- dry_run = [:noop, :dryrun, :dry_run].any? {|sym| global_options[sym] || options[sym] }
173
+ dry_run = global_options[:dry] || options[:dry]
180
174
  $app.dry_run! if dry_run
181
175
 
182
176
  if options[:pass]
@@ -189,6 +183,35 @@ module OpsWalrus
189
183
  end
190
184
  end
191
185
 
186
+ desc "Reboot one or more remote hosts"
187
+ long_desc 'Reboot one or more remote hosts'
188
+ command :reboot do |c|
189
+ # dry run
190
+ c.switch :dry, desc: "Perform a dry run"
191
+
192
+ c.action do |global_options, options, args|
193
+ $app.set_log_level(global_options[:loudest] && :trace || global_options[:louder] && :debug || global_options[:loud] && :info || :warn)
194
+
195
+ hosts = global_options[:hosts]
196
+ $app.set_inventory_hosts(hosts)
197
+
198
+ tags = global_options[:tags]
199
+ $app.set_inventory_tags(tags)
200
+
201
+ id_files = global_options[:id]
202
+ id_files = OpsWalrus.env_specified_age_ids if id_files.empty?
203
+
204
+ $app.set_identity_files(id_files)
205
+
206
+ dry_run = global_options[:dry] || options[:dry]
207
+ $app.dry_run! if dry_run
208
+
209
+ exit_status = $app.reboot()
210
+
211
+ exit_now!("error", exit_status) unless exit_status == 0
212
+ end
213
+ end
214
+
192
215
  desc 'Run an operation from a package'
193
216
  long_desc 'Run the specified operation found within the specified package'
194
217
  arg 'args', :multiple
@@ -196,17 +219,16 @@ module OpsWalrus
196
219
  c.switch [:b, :bundle], desc: "Update bundle prior to running the specified operation"
197
220
  c.switch :pass, desc: "Prompt for a sudo password"
198
221
  c.switch :script, desc: "Script mode"
222
+ c.switch [:r, :remote], desc: "Run the operation on the remote hosts"
199
223
 
200
224
  c.flag [:u, :user], desc: "Specify the user that the operation will run as"
201
225
  c.flag [:p, :params], desc: "Either specify a file that contains JSON OR specify a JSON encoded string. In both cases, the JSON represents the runtime arguments (i.e. the params) for the operation. The JSON string must conform to the params schema for the operation being run."
202
226
 
203
227
  # dry run
204
- c.switch :noop, desc: "Perform a dry run"
205
- c.switch :dryrun, desc: "Perform a dry run"
206
- c.switch :dry_run, desc: "Perform a dry run"
228
+ c.switch :dry, desc: "Perform a dry run"
207
229
 
208
230
  c.action do |global_options, options, args|
209
- $app.set_log_level(global_options[:trace] && :trace || global_options[:debug] && :debug || global_options[:verbose] && :info || :warn)
231
+ $app.set_log_level(global_options[:loudest] && :trace || global_options[:louder] && :debug || global_options[:loud] && :info || :warn)
210
232
 
211
233
  hosts = global_options[:hosts]
212
234
  $app.set_inventory_hosts(hosts)
@@ -225,7 +247,7 @@ module OpsWalrus
225
247
 
226
248
  $app.set_identity_files(id_files)
227
249
 
228
- dry_run = [:noop, :dryrun, :dry_run].any? {|sym| global_options[sym] || options[sym] }
250
+ dry_run = global_options[:dry] || options[:dry]
229
251
  $app.dry_run! if dry_run
230
252
 
231
253
  if options[:pass]
@@ -236,7 +258,11 @@ module OpsWalrus
236
258
  $app.script_mode!
237
259
  end
238
260
 
239
- exit_status = $app.run(args, update_bundle: options[:bundle])
261
+ exit_status = if options[:remote]
262
+ $app.run_remote(args, update_bundle: options[:bundle])
263
+ else
264
+ $app.run(args, update_bundle: options[:bundle])
265
+ end
240
266
 
241
267
  exit_now!("error", exit_status) unless exit_status == 0
242
268
  end
@@ -250,7 +276,7 @@ module OpsWalrus
250
276
  long_desc 'Download and bundle the latest versions of dependencies for the current package'
251
277
  c.command :update do |update|
252
278
  update.action do |global_options, options, args|
253
- $app.set_log_level(global_options[:trace] && :trace || global_options[:debug] && :debug || global_options[:verbose] && :info || :warn)
279
+ $app.set_log_level(global_options[:loudest] && :trace || global_options[:louder] && :debug || global_options[:loud] && :info || :warn)
254
280
 
255
281
  $app.bundle_update
256
282
  end
@@ -270,7 +296,7 @@ module OpsWalrus
270
296
  unzip.flag [:o, :output], desc: "Specify the output directory"
271
297
 
272
298
  unzip.action do |global_options, options, args|
273
- $app.set_log_level(global_options[:trace] && :trace || global_options[:debug] && :debug || global_options[:verbose] && :info || :warn)
299
+ $app.set_log_level(global_options[:loudest] && :trace || global_options[:louder] && :debug || global_options[:loud] && :info || :warn)
274
300
 
275
301
  output_dir = options[:output]
276
302
  zip_file_path = args.first
@@ -81,6 +81,13 @@ module OpsWalrus
81
81
  @_host.to_s
82
82
  end
83
83
 
84
+ def _invoke_remote(ops_file, *args, **kwargs)
85
+ puts "boom"
86
+ is_invocation_a_call_to_package_in_bundle_dir = true
87
+ invocation_context = RemoteImportInvocationContext.new(@runtime_env, self, ops_file, is_invocation_a_call_to_package_in_bundle_dir, ops_prompt_for_sudo_password: !!ssh_password)
88
+ invocation_context._invoke(*args, **kwargs)
89
+ end
90
+
84
91
  # returns [stdout, stderr, exit_status]
85
92
  def _bootstrap_host(print_report = true)
86
93
  # copy over bootstrap shell script
@@ -119,7 +126,7 @@ module OpsWalrus
119
126
  raise Error, "Unable to upload ops bundle to remote host" unless upload_success
120
127
 
121
128
  stdout, _stderr, exit_status = @_host.run_ops(:bundle, "unzip tmpops.zip", in_bundle_root_dir: false)
122
- raise Error, "Unable to unzip ops bundle on remote host" unless exit_status == 0
129
+ raise Error, "Unable to unzip ops bundle on remote host: #{stdout}" unless exit_status == 0
123
130
  tmp_bundle_root_dir = stdout.strip
124
131
  @_host.set_ssh_session_tmp_bundle_root_dir(tmp_bundle_root_dir)
125
132
 
@@ -143,6 +150,58 @@ module OpsWalrus
143
150
 
144
151
 
145
152
  module HostDSL
153
+ # delay: integer? # default: 1 - 1 second delay before reboot
154
+ # sync: boolean? # default: true - wait for the remote host to become available again before returning success/failure
155
+ # timeout: integer? # default: 300 - 300 seconds (5 minutes)
156
+ def reboot(delay: 1, sync: true, timeout: 300)
157
+ delay = 1 if delay < 1
158
+
159
+ desc "Rebooting #{to_s} (alias=#{self.alias})"
160
+ reboot_success = sh? 'sudo /bin/sh -c "(sleep {{ delay }} && reboot) &"'.mustache
161
+ puts reboot_success
162
+
163
+ reconnect_time = nil
164
+ reconnect_success = if sync
165
+ desc "Waiting for #{to_s} (alias=#{self.alias}) to finish rebooting"
166
+ initial_reconnect_delay = delay + 10
167
+ sleep initial_reconnect_delay
168
+
169
+ reconnected = false
170
+ give_up = false
171
+ t1 = Time.now
172
+ until reconnected || give_up
173
+ begin
174
+ reconnected = sh?('true')
175
+ # while trying to reconnect, we expect the following exceptions:
176
+ # 1. Net::SSH::Disconnect < Net::SSH::Exception with message: "connection closed by remote host"
177
+ # 2. Errno::ECONNRESET < SystemCallError with message: "Connection reset by peer"
178
+ # 3. Errno::ECONNREFUSED < SystemCallError with message: "Connection refused - connect(2) for 192.168.56.10:22"
179
+ rescue Net::SSH::Disconnect, Errno::ECONNRESET, Errno::ECONNREFUSED => e
180
+ # noop; we expect these while we're trying to reconnect
181
+ rescue => e
182
+ puts "#{e.class} < #{e.class.superclass}"
183
+ puts e.message
184
+ puts e.backtrace.take(5).join("\n")
185
+ end
186
+
187
+ wait_time_elapsed_in_seconds = Time.now - t1
188
+ give_up = wait_time_elapsed_in_seconds > timeout
189
+ sleep 5
190
+ end
191
+ reconnect_time = initial_reconnect_delay + (Time.now - t1)
192
+ reconnected
193
+ else
194
+ false
195
+ end
196
+
197
+ {
198
+ success: reboot_success && (sync == reconnect_success),
199
+ rebooted: reboot_success,
200
+ reconnected: reconnect_success,
201
+ reboot_duration: reconnect_time
202
+ }
203
+ end
204
+
146
205
  # runs the given command
147
206
  # returns the stdout from the command
148
207
  def sh(desc_or_cmd = nil, cmd = nil, input: nil, &block)
@@ -163,7 +222,7 @@ module OpsWalrus
163
222
  end
164
223
 
165
224
  # returns the tuple: [stdout, stderr, exit_status]
166
- def shell!(desc_or_cmd = nil, cmd = nil, block = nil, input: nil, log_level: nil, ops_prompt_for_sudo_password: false)
225
+ def shell!(desc_or_cmd = nil, cmd = nil, block = nil, input: nil, ops_prompt_for_sudo_password: false)
167
226
  # description = nil
168
227
 
169
228
  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
@@ -179,7 +238,7 @@ module OpsWalrus
179
238
  else
180
239
  offset = 3 # 3, because 1 references the stack frame corresponding to the caller of WalrusLang.eval,
181
240
  # 2 references the stack frame corresponding to the caller of shell!,
182
- # and 3 references the stack frame corresponding to teh caller of either sh/sh?/shell
241
+ # and 3 references the stack frame corresponding to the caller of either sh/sh?/shell
183
242
  WalrusLang.eval(cmd, offset)
184
243
  end
185
244
  else
@@ -190,28 +249,24 @@ module OpsWalrus
190
249
  #cmd = Shellwords.escape(cmd)
191
250
 
192
251
  cmd_id = Random.uuid.split('-').first
193
- # if App.instance.report_mode?
194
252
  output_block = StringIO.open do |io|
195
- io.print Style.blue(host)
196
- io.print " (#{Style.blue(self.alias)})" if self.alias
197
- io.print " | #{Style.magenta(description)}" if description
198
- io.puts
199
- io.print Style.yellow(cmd_id)
200
- io.print Style.green.bold(" > ")
201
- io.puts Style.yellow(cmd)
253
+ if App.instance.info? # this is true if log_level is trace, debug, info
254
+ io.print Style.blue(host)
255
+ io.print " (#{Style.blue(self.alias)})" if self.alias
256
+ io.print " | #{Style.magenta(description)}" if description
257
+ io.puts
258
+ io.print Style.yellow(cmd_id)
259
+ io.print Style.green.bold(" > ")
260
+ io.puts Style.yellow(cmd)
261
+ elsif App.instance.warn? && description
262
+ io.print Style.blue(host)
263
+ io.print " (#{Style.blue(self.alias)})" if self.alias
264
+ io.print " | #{Style.magenta(description)}" if description
265
+ io.puts
266
+ end
202
267
  io.string
203
268
  end
204
- puts output_block
205
-
206
- # puts Style.green("*" * 80)
207
- # if self.alias
208
- # print "[#{Style.blue(self.alias)} | #{Style.blue(host)}] "
209
- # else
210
- # print "[#{Style.blue(host)}] "
211
- # end
212
- # print "#{description}: " if description
213
- # puts Style.yellow("[#{cmd_id}] #{cmd}")
214
- # end
269
+ puts output_block unless output_block.empty?
215
270
 
216
271
  return unless cmd && !cmd.strip.empty?
217
272
 
@@ -226,27 +281,37 @@ module OpsWalrus
226
281
  seconds = t2 - t1
227
282
 
228
283
  output_block = StringIO.open do |io|
229
- if App.instance.info? || log_level == :info
230
- io.puts Style.cyan(out)
231
- io.puts Style.red(err)
232
- elsif App.instance.debug? || log_level == :debug
233
- io.puts Style.cyan(out)
234
- io.puts Style.red(err)
235
- elsif App.instance.trace? || log_level == :trace
236
- io.puts Style.cyan(out)
237
- io.puts Style.red(err)
238
- end
239
- io.print Style.yellow(cmd_id)
240
- io.print Style.blue(" | Finished in #{seconds} seconds with exit status ")
241
- if exit_status == 0
242
- io.puts Style.green("#{exit_status} (#{exit_status == 0 ? 'success' : 'failure'})")
243
- else
244
- io.puts Style.red("#{exit_status} (#{exit_status == 0 ? 'success' : 'failure'})")
284
+ if App.instance.info? # this is true if log_level is trace, debug, info
285
+ if App.instance.trace?
286
+ io.puts Style.cyan(out)
287
+ io.puts Style.red(err)
288
+ elsif App.instance.debug?
289
+ io.puts Style.cyan(out)
290
+ io.puts Style.red(err)
291
+ elsif App.instance.info?
292
+ # io.puts Style.cyan(out)
293
+ # io.puts Style.red(err)
294
+ end
295
+ io.print Style.yellow(cmd_id)
296
+ io.print Style.blue(" | Finished in #{seconds} seconds with exit status ")
297
+ if exit_status == 0
298
+ io.puts Style.green("#{exit_status} (success)")
299
+ else
300
+ io.puts Style.red("#{exit_status} (failure)")
301
+ end
302
+ io.puts Style.green("*" * 80)
303
+ elsif App.instance.warn? && description
304
+ io.print Style.blue(" | Finished in #{seconds} seconds with exit status ")
305
+ if exit_status == 0
306
+ io.puts Style.green("#{exit_status} (success)")
307
+ else
308
+ io.puts Style.red("#{exit_status} (failure)")
309
+ end
310
+ io.puts Style.green("*" * 80)
245
311
  end
246
- io.puts Style.green("*" * 80)
247
312
  io.string
248
313
  end
249
- puts output_block
314
+ puts output_block unless output_block.empty?
250
315
 
251
316
  [out, err, exit_status]
252
317
  end
@@ -262,18 +327,18 @@ module OpsWalrus
262
327
  # cmd = "OPS_GEM=\"#{OPS_GEM}\" OPSWALRUS_LOCAL_HOSTNAME='#{local_hostname_for_remote_host}'; $OPS_GEM exec --conservative -g opswalrus ops"
263
328
  cmd = "OPSWALRUS_LOCAL_HOSTNAME='#{local_hostname_for_remote_host}' eval #{OPS_CMD}"
264
329
  if App.instance.trace?
265
- cmd << " --trace"
330
+ cmd << " --loudest"
266
331
  elsif App.instance.debug?
267
- cmd << " --debug"
332
+ cmd << " --louder"
268
333
  elsif App.instance.info?
269
- cmd << " --verbose"
334
+ cmd << " --loud"
270
335
  end
271
336
  cmd << " #{ops_command.to_s}"
272
337
  cmd << " #{ops_command_options.to_s}" if ops_command_options
273
338
  cmd << " #{@tmp_bundle_root_dir}" if in_bundle_root_dir
274
339
  cmd << " #{command_arguments}" unless command_arguments.empty?
275
340
 
276
- shell!(cmd, log_level: :info, ops_prompt_for_sudo_password: ops_prompt_for_sudo_password)
341
+ shell!(cmd, ops_prompt_for_sudo_password: ops_prompt_for_sudo_password)
277
342
  end
278
343
 
279
344
  def desc(msg)
@@ -8,9 +8,7 @@ module OpsWalrus
8
8
 
9
9
  attr_accessor :input_mappings # Hash[ String | Regex => String ]
10
10
 
11
- # log_level is one of: :fatal, :error, :warn, :info, :debug, :trace
12
- def initialize(mapping, log_level = nil)
13
- @log_level = log_level
11
+ def initialize(mapping)
14
12
  @input_mappings = mapping
15
13
  end
16
14
 
@@ -63,7 +61,7 @@ module OpsWalrus
63
61
  if new_mapping.empty? || new_mapping == @input_mappings
64
62
  yield self
65
63
  else
66
- yield ScopedMappingInteractionHandler.new(new_mapping, @log_level)
64
+ yield ScopedMappingInteractionHandler.new(new_mapping)
67
65
  end
68
66
  end
69
67
 
@@ -110,12 +110,12 @@ module OpsWalrus
110
110
  Invocation::Error.new(e)
111
111
  end
112
112
 
113
- if result.failure?
114
- App.instance.debug "Ops script error details:"
115
- App.instance.debug "Error: #{result.value}"
116
- App.instance.debug "Status code: #{result.exit_status}"
117
- App.instance.debug @entry_point_ops_file.script.to_s
118
- end
113
+ # if result.failure?
114
+ # App.instance.debug "Ops script error details:"
115
+ # App.instance.debug "Error: #{result.value}"
116
+ # App.instance.debug "Status code: #{result.exit_status}"
117
+ # App.instance.debug @entry_point_ops_file.script.to_s
118
+ # end
119
119
 
120
120
  result
121
121
  end
@@ -55,6 +55,9 @@ module OpsWalrus
55
55
  sshkit_hosts = hosts.map(&:sshkit_host)
56
56
  sshkit_host_to_ops_host_map = sshkit_hosts.zip(hosts).to_h
57
57
  ops_file_script = local_host = self
58
+
59
+ results_lock = Thread::Mutex.new
60
+ results = {}
58
61
  # on sshkit_hosts do |sshkit_host|
59
62
  SSHKit::Coordinator.new(sshkit_hosts).each(in: kwargs[:in] || :parallel) do |sshkit_host|
60
63
  # in this context, self is an instance of one of the subclasses of SSHKit::Backend::Abstract, e.g. SSHKit::Backend::Netssh
@@ -73,6 +76,10 @@ module OpsWalrus
73
76
  # we run the block in the context of the host proxy object, s.t. `self` within the block evaluates to the host proxy object
74
77
  retval = host.instance_exec(local_host, &block) # local_host is passed as the argument to the block
75
78
 
79
+ results_lock.synchronize do
80
+ results[host] = retval
81
+ end
82
+
76
83
  retval
77
84
  rescue SSHKit::Command::Failed => e
78
85
  App.instance.error "[!] Command failed:"
@@ -102,6 +109,7 @@ module OpsWalrus
102
109
  end
103
110
  end # runtime_env.handle_input
104
111
  end # SSHKit::Coordinator
112
+ results
105
113
  end # def ssh
106
114
 
107
115
  def ssh(*args, **kwargs, &block)
@@ -111,6 +119,9 @@ module OpsWalrus
111
119
  sshkit_hosts = hosts.map(&:sshkit_host)
112
120
  sshkit_host_to_ops_host_map = sshkit_hosts.zip(hosts).to_h
113
121
  ops_file_script = local_host = self
122
+
123
+ results_lock = Thread::Mutex.new
124
+ results = {}
114
125
  # bootstrap_shell_script = BootstrapLinuxHostShellScript
115
126
  # on sshkit_hosts do |sshkit_host|
116
127
  SSHKit::Coordinator.new(sshkit_hosts).each(in: kwargs[:in] || :parallel) do |sshkit_host|
@@ -134,6 +145,10 @@ module OpsWalrus
134
145
  puts "Failed to bootstrap #{host}. Unable to run operation."
135
146
  end
136
147
 
148
+ results_lock.synchronize do
149
+ results[host] = retval
150
+ end
151
+
137
152
  retval
138
153
  rescue SSHKit::Command::Failed => e
139
154
  App.instance.error "[!] Command failed:"
@@ -163,6 +178,7 @@ module OpsWalrus
163
178
  end
164
179
  end # runtime_env.handle_input
165
180
  end # SSHKit::Coordinator
181
+ results
166
182
  end # def ssh
167
183
 
168
184
  def current_dir
@@ -247,7 +263,7 @@ module OpsWalrus
247
263
  end
248
264
 
249
265
  # returns the tuple: [stdout, stderr, exit_status]
250
- def shell!(desc_or_cmd = nil, cmd = nil, block = nil, input: nil, log_level: nil)
266
+ def shell!(desc_or_cmd = nil, cmd = nil, block = nil, input: nil)
251
267
  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
252
268
 
253
269
  description = desc_or_cmd if cmd || block
@@ -261,7 +277,7 @@ module OpsWalrus
261
277
  else
262
278
  offset = 3 # 3, because 1 references the stack frame corresponding to the caller of WalrusLang.eval,
263
279
  # 2 references the stack frame corresponding to the caller of shell!,
264
- # and 3 references the stack frame corresponding to teh caller of either sh/sh?/shell
280
+ # and 3 references the stack frame corresponding to the caller of either sh/sh?/shell
265
281
  WalrusLang.eval(cmd, offset)
266
282
  end
267
283
  else
@@ -269,7 +285,7 @@ module OpsWalrus
269
285
  end
270
286
  #cmd = Shellwords.escape(cmd)
271
287
 
272
- report_on(@runtime_env.local_hostname, description, cmd, log_level: log_level) do
288
+ report_on(@runtime_env.local_hostname, description, cmd) do
273
289
  if App.instance.dry_run?
274
290
  ["", "", 0]
275
291
  else
@@ -284,19 +300,25 @@ module OpsWalrus
284
300
  end
285
301
  end
286
302
 
287
- def report_on(hostname, description = nil, cmd, log_level: nil)
303
+ def report_on(hostname, description = nil, cmd)
288
304
  cmd_id = Random.uuid.split('-').first
289
305
 
290
306
  output_block = StringIO.open do |io|
291
- io.print Style.blue(hostname)
292
- io.print " | #{Style.magenta(description)}" if description
293
- io.puts
294
- io.print Style.yellow(cmd_id)
295
- io.print Style.green.bold(" > ")
296
- io.puts Style.yellow(cmd)
307
+ if App.instance.info? # this is true if log_level is trace, debug, info
308
+ io.print Style.blue(hostname)
309
+ io.print " | #{Style.magenta(description)}" if description
310
+ io.puts
311
+ io.print Style.yellow(cmd_id)
312
+ io.print Style.green.bold(" > ")
313
+ io.puts Style.yellow(cmd)
314
+ elsif App.instance.warn? && description
315
+ io.print Style.blue(hostname)
316
+ io.print " | #{Style.magenta(description)}" if description
317
+ io.puts
318
+ end
297
319
  io.string
298
320
  end
299
- puts output_block
321
+ puts output_block unless output_block.empty?
300
322
 
301
323
  t1 = Time.now
302
324
  out, err, exit_status = yield
@@ -304,27 +326,37 @@ module OpsWalrus
304
326
  seconds = t2 - t1
305
327
 
306
328
  output_block = StringIO.open do |io|
307
- if App.instance.info? || log_level == :info
308
- io.puts Style.cyan(out)
309
- io.puts Style.red(err)
310
- elsif App.instance.debug? || log_level == :debug
311
- io.puts Style.cyan(out)
312
- io.puts Style.red(err)
313
- elsif App.instance.trace? || log_level == :trace
314
- io.puts Style.cyan(out)
315
- io.puts Style.red(err)
316
- end
317
- io.print Style.yellow(cmd_id)
318
- io.print Style.blue(" | Finished in #{seconds} seconds with exit status ")
319
- if exit_status == 0
320
- io.puts Style.green("#{exit_status} (#{exit_status == 0 ? 'success' : 'failure'})")
321
- else
322
- io.puts Style.red("#{exit_status} (#{exit_status == 0 ? 'success' : 'failure'})")
329
+ if App.instance.info? # this is true if log_level is trace, debug, info
330
+ if App.instance.trace?
331
+ io.puts Style.cyan(out)
332
+ io.puts Style.red(err)
333
+ elsif App.instance.debug?
334
+ io.puts Style.cyan(out)
335
+ io.puts Style.red(err)
336
+ elsif App.instance.info?
337
+ # io.puts Style.cyan(out)
338
+ # io.puts Style.red(err)
339
+ end
340
+ io.print Style.yellow(cmd_id)
341
+ io.print Style.blue(" | Finished in #{seconds} seconds with exit status ")
342
+ if exit_status == 0
343
+ io.puts Style.green("#{exit_status} (success)")
344
+ else
345
+ io.puts Style.red("#{exit_status} (failure)")
346
+ end
347
+ io.puts Style.green("*" * 80)
348
+ elsif App.instance.warn? && description
349
+ io.print Style.blue(" | Finished in #{seconds} seconds with exit status ")
350
+ if exit_status == 0
351
+ io.puts Style.green("#{exit_status} (success)")
352
+ else
353
+ io.puts Style.red("#{exit_status} (failure)")
354
+ end
355
+ io.puts Style.green("*" * 80)
323
356
  end
324
- io.puts Style.green("*" * 80)
325
357
  io.string
326
358
  end
327
- puts output_block
359
+ puts output_block unless output_block.empty?
328
360
 
329
361
  [out, err, exit_status]
330
362
  end
@@ -1,5 +1,7 @@
1
1
  require 'json'
2
2
  require 'pathname'
3
+ require 'active_support'
4
+ require 'active_support/core_ext/hash'
3
5
 
4
6
  class String
5
7
  def escape_single_quotes
@@ -1,3 +1,3 @@
1
1
  module OpsWalrus
2
- VERSION = "1.0.51"
2
+ VERSION = "1.0.53"
3
3
  end
data/opswalrus.gemspec CHANGED
@@ -32,6 +32,7 @@ Gem::Specification.new do |spec|
32
32
  spec.require_paths = ["lib"]
33
33
 
34
34
  # gem dependencies
35
+ spec.add_dependency "activesupport", "~> 7.0"
35
36
  spec.add_dependency "binding_of_caller", "~> 1.0"
36
37
  spec.add_dependency "citrus", "~> 3.0"
37
38
  spec.add_dependency "gli", "~> 2.21"
metadata CHANGED
@@ -1,15 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: opswalrus
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.51
4
+ version: 1.0.53
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-20 00:00:00.000000000 Z
11
+ date: 2023-09-22 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activesupport
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '7.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '7.0'
13
27
  - !ruby/object:Gem::Dependency
14
28
  name: binding_of_caller
15
29
  requirement: !ruby/object:Gem::Requirement
@@ -185,6 +199,8 @@ files:
185
199
  - exe/ops
186
200
  - lib/opswalrus.rb
187
201
  - lib/opswalrus/_bootstrap.ops
202
+ - lib/opswalrus/_reboot.ops
203
+ - lib/opswalrus/_run_remote.ops
188
204
  - lib/opswalrus/_shell.ops
189
205
  - lib/opswalrus/app.rb
190
206
  - lib/opswalrus/bootstrap.sh