opswalrus 1.0.52 → 1.0.54

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: b1ce678751d6d62f4a9b5e40eb6127c22e2e664c6641023ce8df4135609fb73e
4
- data.tar.gz: a2747abc2711e77a47371f7c16d1b5104d70b10419b09cd3e992e892cea2d6ca
3
+ metadata.gz: 50b82ac565a56bb1585653a90c4c0c09b06ba3490c3121da8717e9bdf085f48b
4
+ data.tar.gz: f38e88fa6db2769cd7ae59291ce28f337c1ca8a9bcd00509f8ccc00cd47b0890
5
5
  SHA512:
6
- metadata.gz: 5ec4e2b6a547e394b0290a47dafa7cc13960a9c21cef7e1a7edc4d20900cd304a1e0c828025df68f8e7e2e6ce22e3f48882fe44efcaaf1a25e3c74d357981071
7
- data.tar.gz: 386afc9a49856131615ff5c5e0c54a1934bf8458d3a77f7f2c6e1da6479343c54a0be8436f8a08beec4e89d6f1b9019e463bc96a5c169f639a7061838f8f240f
6
+ metadata.gz: 751dfb54af8521b4e33a45cd8d38c0aa0b2c433006bd7030e412605584ffb4b7cba44d49fe26b64c7c2c4d0f2cea08a53cd02aea40c1d2341de7d20fbbe1ac35
7
+ data.tar.gz: a52102d80e0b0f966d7b357173f87cafcb6ddcfdf0e9d0b3aa369e115c5b7da03ef0ce1fb6ba8d25486ac296e2f3e06dc717ab0fe61f96cf3c904b0a14b3d87c
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.52)
4
+ opswalrus (1.0.54)
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,11 @@
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
+ _invoke_remote(params.ops_file, *params.operation_kv_args)
11
+ 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,27 +247,45 @@ module OpsWalrus
243
247
  1
244
248
  end
245
249
 
246
- def reboot()
247
- set_pwd(__FILE__.to_pathname.dirname)
248
- shell_ops_file = OpsFile.new(self, __FILE__.to_pathname.dirname.join("_reboot.ops"))
249
- op = OperationRunner.new(self, shell_ops_file)
250
- result = op.run([], params_json_hash: @params)
251
- puts "result class=#{result.class}"
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})
252
270
  exit_status = result.exit_status
253
- stdout = JSON.pretty_generate(result.value)
254
- output = if exit_status == 0
255
- Style.green(stdout)
256
- else
257
- Style.red(stdout)
258
- end
259
- puts output
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
+
260
280
  exit_status
261
281
  rescue Error => e
262
282
  puts "Error: #{e.message}"
263
283
  1
284
+ ensure
285
+ FileUtils.remove_entry(tmp_bundle_root_dir) if tmp_bundle_root_dir
264
286
  end
265
287
 
266
- # args is of the form ["github.com/davidkellis/my-package/sub-package1", "operation1", "arg1:val1", "arg2:val2", "arg3:val3"]
288
+ # package_operation_and_args is of the form ["github.com/davidkellis/my-package/sub-package1", "operation1", "arg1:val1", "arg2:val2", "arg3:val3"]
267
289
  # if the first argument is the path to a .ops file, then treat it as a local path, and add the containing package
268
290
  # to the load path
269
291
  # otherwise, copy the
@@ -56,7 +56,11 @@ module OpsWalrus
56
56
  # end
57
57
 
58
58
  def update()
59
- delete_pwd_bundle_directory
59
+ # delete_pwd_bundle_directory # this was causing problems when we do: ops run -r -b ... because a dynamic package reference was being
60
+ # downloaded to the bundle dir, and then update was being called afterward, which was blowing away
61
+ # the entire bundle dir, so when the bundle dir was copied to the remote host, the ops file being
62
+ # invoked was not present on the remote host, since it had never been copied over as a result
63
+ # of the bundle dir being blown away after the dynamic package reference had been downloaded
60
64
  ensure_pwd_bundle_directory_exists
61
65
 
62
66
  package_yaml_files = pwd.glob("./**/package.yaml") - pwd.glob("./**/#{BUNDLE_DIR}/**/package.yaml")
@@ -204,6 +208,8 @@ module OpsWalrus
204
208
 
205
209
  destination_package_path ||= dynamic_package_path_for_git_package(package_url, version)
206
210
 
211
+ App.instance.debug "Download git package #{package_url} version #{version.inspect} to #{destination_package_path}"
212
+
207
213
  return destination_package_path if destination_package_path.exist?
208
214
 
209
215
  if version
@@ -212,6 +218,8 @@ module OpsWalrus
212
218
  ::Git.clone(package_url, destination_package_path, config: ['submodule.recurse=true'])
213
219
  end
214
220
 
221
+ App.instance.debug "Downloaded git package #{package_url} version #{version.inspect} to #{destination_package_path}"
222
+
215
223
  destination_package_path
216
224
  end
217
225
 
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]
@@ -193,12 +187,10 @@ module OpsWalrus
193
187
  long_desc 'Reboot one or more remote hosts'
194
188
  command :reboot do |c|
195
189
  # dry run
196
- c.switch :noop, desc: "Perform a dry run"
197
- c.switch :dryrun, desc: "Perform a dry run"
198
- c.switch :dry_run, desc: "Perform a dry run"
190
+ c.switch :dry, desc: "Perform a dry run"
199
191
 
200
192
  c.action do |global_options, options, args|
201
- $app.set_log_level(global_options[:trace] && :trace || global_options[:debug] && :debug || global_options[:verbose] && :info || :warn)
193
+ $app.set_log_level(global_options[:loudest] && :trace || global_options[:louder] && :debug || global_options[:loud] && :info || :warn)
202
194
 
203
195
  hosts = global_options[:hosts]
204
196
  $app.set_inventory_hosts(hosts)
@@ -211,7 +203,7 @@ module OpsWalrus
211
203
 
212
204
  $app.set_identity_files(id_files)
213
205
 
214
- dry_run = [:noop, :dryrun, :dry_run].any? {|sym| global_options[sym] || options[sym] }
206
+ dry_run = global_options[:dry] || options[:dry]
215
207
  $app.dry_run! if dry_run
216
208
 
217
209
  exit_status = $app.reboot()
@@ -227,17 +219,16 @@ module OpsWalrus
227
219
  c.switch [:b, :bundle], desc: "Update bundle prior to running the specified operation"
228
220
  c.switch :pass, desc: "Prompt for a sudo password"
229
221
  c.switch :script, desc: "Script mode"
222
+ c.switch [:r, :remote], desc: "Run the operation on the remote hosts"
230
223
 
231
224
  c.flag [:u, :user], desc: "Specify the user that the operation will run as"
232
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."
233
226
 
234
227
  # dry run
235
- c.switch :noop, desc: "Perform a dry run"
236
- c.switch :dryrun, desc: "Perform a dry run"
237
- c.switch :dry_run, desc: "Perform a dry run"
228
+ c.switch :dry, desc: "Perform a dry run"
238
229
 
239
230
  c.action do |global_options, options, args|
240
- $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)
241
232
 
242
233
  hosts = global_options[:hosts]
243
234
  $app.set_inventory_hosts(hosts)
@@ -256,7 +247,7 @@ module OpsWalrus
256
247
 
257
248
  $app.set_identity_files(id_files)
258
249
 
259
- dry_run = [:noop, :dryrun, :dry_run].any? {|sym| global_options[sym] || options[sym] }
250
+ dry_run = global_options[:dry] || options[:dry]
260
251
  $app.dry_run! if dry_run
261
252
 
262
253
  if options[:pass]
@@ -267,7 +258,11 @@ module OpsWalrus
267
258
  $app.script_mode!
268
259
  end
269
260
 
270
- 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
271
266
 
272
267
  exit_now!("error", exit_status) unless exit_status == 0
273
268
  end
@@ -281,7 +276,7 @@ module OpsWalrus
281
276
  long_desc 'Download and bundle the latest versions of dependencies for the current package'
282
277
  c.command :update do |update|
283
278
  update.action do |global_options, options, args|
284
- $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)
285
280
 
286
281
  $app.bundle_update
287
282
  end
@@ -301,7 +296,7 @@ module OpsWalrus
301
296
  unzip.flag [:o, :output], desc: "Specify the output directory"
302
297
 
303
298
  unzip.action do |global_options, options, args|
304
- $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)
305
300
 
306
301
  output_dir = options[:output]
307
302
  zip_file_path = args.first
@@ -81,6 +81,12 @@ module OpsWalrus
81
81
  @_host.to_s
82
82
  end
83
83
 
84
+ def _invoke_remote(ops_file, *args, **kwargs)
85
+ App.instance.debug "Remove invoke #{ops_file.relative_path_to_app_pwd} on host `#{to_s}` with args:\n #{args.inspect}\nkwargs:\n #{kwargs.inspect}"
86
+
87
+ RemoteInvocation.new(self, ops_file, ops_prompt_for_sudo_password: !!ssh_password).invoke(*args, **kwargs)
88
+ end
89
+
84
90
  # returns [stdout, stderr, exit_status]
85
91
  def _bootstrap_host(print_report = true)
86
92
  # copy over bootstrap shell script
@@ -119,7 +125,7 @@ module OpsWalrus
119
125
  raise Error, "Unable to upload ops bundle to remote host" unless upload_success
120
126
 
121
127
  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
128
+ raise Error, "Unable to unzip ops bundle on remote host: #{stdout}" unless exit_status == 0
123
129
  tmp_bundle_root_dir = stdout.strip
124
130
  @_host.set_ssh_session_tmp_bundle_root_dir(tmp_bundle_root_dir)
125
131
 
@@ -168,7 +174,8 @@ module OpsWalrus
168
174
  # while trying to reconnect, we expect the following exceptions:
169
175
  # 1. Net::SSH::Disconnect < Net::SSH::Exception with message: "connection closed by remote host"
170
176
  # 2. Errno::ECONNRESET < SystemCallError with message: "Connection reset by peer"
171
- rescue Net::SSH::Disconnect, Errno::ECONNRESET => e
177
+ # 3. Errno::ECONNREFUSED < SystemCallError with message: "Connection refused - connect(2) for 192.168.56.10:22"
178
+ rescue Net::SSH::Disconnect, Errno::ECONNRESET, Errno::ECONNREFUSED => e
172
179
  # noop; we expect these while we're trying to reconnect
173
180
  rescue => e
174
181
  puts "#{e.class} < #{e.class.superclass}"
@@ -214,7 +221,7 @@ module OpsWalrus
214
221
  end
215
222
 
216
223
  # returns the tuple: [stdout, stderr, exit_status]
217
- def shell!(desc_or_cmd = nil, cmd = nil, block = nil, input: nil, log_level: nil, ops_prompt_for_sudo_password: false)
224
+ def shell!(desc_or_cmd = nil, cmd = nil, block = nil, input: nil, ops_prompt_for_sudo_password: false)
218
225
  # description = nil
219
226
 
220
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
@@ -241,28 +248,24 @@ module OpsWalrus
241
248
  #cmd = Shellwords.escape(cmd)
242
249
 
243
250
  cmd_id = Random.uuid.split('-').first
244
- # if App.instance.report_mode?
245
251
  output_block = StringIO.open do |io|
246
- io.print Style.blue(host)
247
- io.print " (#{Style.blue(self.alias)})" if self.alias
248
- io.print " | #{Style.magenta(description)}" if description
249
- io.puts
250
- io.print Style.yellow(cmd_id)
251
- io.print Style.green.bold(" > ")
252
- io.puts Style.yellow(cmd)
252
+ if App.instance.info? # this is true if log_level is trace, debug, info
253
+ io.print Style.blue(host)
254
+ io.print " (#{Style.blue(self.alias)})" if self.alias
255
+ io.print " | #{Style.magenta(description)}" if description
256
+ io.puts
257
+ io.print Style.yellow(cmd_id)
258
+ io.print Style.green.bold(" > ")
259
+ io.puts Style.yellow(cmd)
260
+ elsif App.instance.warn? && description
261
+ io.print Style.blue(host)
262
+ io.print " (#{Style.blue(self.alias)})" if self.alias
263
+ io.print " | #{Style.magenta(description)}" if description
264
+ io.puts
265
+ end
253
266
  io.string
254
267
  end
255
- puts output_block
256
-
257
- # puts Style.green("*" * 80)
258
- # if self.alias
259
- # print "[#{Style.blue(self.alias)} | #{Style.blue(host)}] "
260
- # else
261
- # print "[#{Style.blue(host)}] "
262
- # end
263
- # print "#{description}: " if description
264
- # puts Style.yellow("[#{cmd_id}] #{cmd}")
265
- # end
268
+ puts output_block unless output_block.empty?
266
269
 
267
270
  return unless cmd && !cmd.strip.empty?
268
271
 
@@ -277,27 +280,37 @@ module OpsWalrus
277
280
  seconds = t2 - t1
278
281
 
279
282
  output_block = StringIO.open do |io|
280
- if App.instance.info? || log_level == :info
281
- io.puts Style.cyan(out)
282
- io.puts Style.red(err)
283
- elsif App.instance.debug? || log_level == :debug
284
- io.puts Style.cyan(out)
285
- io.puts Style.red(err)
286
- elsif App.instance.trace? || log_level == :trace
287
- io.puts Style.cyan(out)
288
- io.puts Style.red(err)
289
- end
290
- io.print Style.yellow(cmd_id)
291
- io.print Style.blue(" | Finished in #{seconds} seconds with exit status ")
292
- if exit_status == 0
293
- io.puts Style.green("#{exit_status} (#{exit_status == 0 ? 'success' : 'failure'})")
294
- else
295
- io.puts Style.red("#{exit_status} (#{exit_status == 0 ? 'success' : 'failure'})")
283
+ if App.instance.info? # this is true if log_level is trace, debug, info
284
+ if App.instance.trace?
285
+ io.puts Style.cyan(out)
286
+ io.puts Style.red(err)
287
+ elsif App.instance.debug?
288
+ io.puts Style.cyan(out)
289
+ io.puts Style.red(err)
290
+ elsif App.instance.info?
291
+ # io.puts Style.cyan(out)
292
+ # io.puts Style.red(err)
293
+ end
294
+ io.print Style.yellow(cmd_id)
295
+ io.print Style.blue(" | Finished in #{seconds} seconds with exit status ")
296
+ if exit_status == 0
297
+ io.puts Style.green("#{exit_status} (success)")
298
+ else
299
+ io.puts Style.red("#{exit_status} (failure)")
300
+ end
301
+ io.puts Style.green("*" * 80)
302
+ elsif App.instance.warn? && description
303
+ io.print Style.blue(" | Finished in #{seconds} seconds with exit status ")
304
+ if exit_status == 0
305
+ io.puts Style.green("#{exit_status} (success)")
306
+ else
307
+ io.puts Style.red("#{exit_status} (failure)")
308
+ end
309
+ io.puts Style.green("*" * 80)
296
310
  end
297
- io.puts Style.green("*" * 80)
298
311
  io.string
299
312
  end
300
- puts output_block
313
+ puts output_block unless output_block.empty?
301
314
 
302
315
  [out, err, exit_status]
303
316
  end
@@ -313,18 +326,18 @@ module OpsWalrus
313
326
  # cmd = "OPS_GEM=\"#{OPS_GEM}\" OPSWALRUS_LOCAL_HOSTNAME='#{local_hostname_for_remote_host}'; $OPS_GEM exec --conservative -g opswalrus ops"
314
327
  cmd = "OPSWALRUS_LOCAL_HOSTNAME='#{local_hostname_for_remote_host}' eval #{OPS_CMD}"
315
328
  if App.instance.trace?
316
- cmd << " --trace"
329
+ cmd << " --loudest"
317
330
  elsif App.instance.debug?
318
- cmd << " --debug"
331
+ cmd << " --louder"
319
332
  elsif App.instance.info?
320
- cmd << " --verbose"
333
+ cmd << " --loud"
321
334
  end
322
335
  cmd << " #{ops_command.to_s}"
323
336
  cmd << " #{ops_command_options.to_s}" if ops_command_options
324
337
  cmd << " #{@tmp_bundle_root_dir}" if in_bundle_root_dir
325
338
  cmd << " #{command_arguments}" unless command_arguments.empty?
326
339
 
327
- shell!(cmd, log_level: :info, ops_prompt_for_sudo_password: ops_prompt_for_sudo_password)
340
+ shell!(cmd, ops_prompt_for_sudo_password: ops_prompt_for_sudo_password)
328
341
  end
329
342
 
330
343
  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
 
@@ -196,4 +196,62 @@ module OpsWalrus
196
196
 
197
197
  end
198
198
 
199
+
200
+
201
+
202
+
203
+ class RemoteInvocation
204
+ def initialize(host_proxy, ops_file, ops_prompt_for_sudo_password: nil)
205
+ @host_proxy = host_proxy
206
+ @ops_file = ops_file
207
+ @ops_prompt_for_sudo_password = ops_prompt_for_sudo_password
208
+ end
209
+
210
+ def invoke(*args, **kwargs)
211
+ # when there are args or kwargs, then the method invocation represents an attempt to run an OpsFile on a remote host,
212
+ # so we want to build up a command and send it to the remote host via HostDSL#run_ops
213
+ remote_run_command_args = @ops_file.relative_path_to_app_pwd.to_s
214
+
215
+ unless args.empty?
216
+ remote_run_command_args << " "
217
+ remote_run_command_args << args.join(" ")
218
+ end
219
+
220
+ begin
221
+ json = JSON.dump(kwargs) unless kwargs.empty?
222
+ if json
223
+ # write the kwargs to a tempfile
224
+ json_kwargs_tempfile = Tempfile.create('ops_invoke_kwargs')
225
+ json_kwargs_tempfile.write(json)
226
+ json_kwargs_tempfile.close() # we want to close the file without unlinking so that we can copy it to the remote host before deleting it
227
+
228
+ # upload the kwargs file to the remote host
229
+ json_kwargs_tempfile_path = json_kwargs_tempfile.path.to_pathname
230
+ remote_json_kwargs_tempfile_basename = json_kwargs_tempfile_path.basename
231
+ @host_proxy.upload(json_kwargs_tempfile_path, remote_json_kwargs_tempfile_basename)
232
+ end
233
+
234
+ # invoke the ops command on the remote host to run the specified ops script on the remote host
235
+ ops_command_options = ""
236
+ ops_command_options << "--pass" if @ops_prompt_for_sudo_password
237
+ ops_command_options << " --params #{remote_json_kwargs_tempfile_basename}" if remote_json_kwargs_tempfile_basename
238
+ retval = if ops_command_options.empty?
239
+ @host_proxy.run_ops(:run, remote_run_command_args, ops_prompt_for_sudo_password: @ops_prompt_for_sudo_password)
240
+ else
241
+ @host_proxy.run_ops(:run, ops_command_options, remote_run_command_args, ops_prompt_for_sudo_password: @ops_prompt_for_sudo_password)
242
+ end
243
+
244
+ retval
245
+ ensure
246
+ if json_kwargs_tempfile
247
+ json_kwargs_tempfile.close rescue nil
248
+ File.unlink(json_kwargs_tempfile) rescue nil
249
+ end
250
+ # todo: make sure this cleanup is present
251
+ if remote_json_kwargs_tempfile_basename
252
+ @host_proxy.execute(:rm, "-f", remote_json_kwargs_tempfile_basename)
253
+ end
254
+ end
255
+ end
256
+ end
199
257
  end
@@ -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
@@ -233,6 +233,13 @@ module OpsWalrus
233
233
  # end
234
234
  # end
235
235
 
236
+ # if ops_file_path is /home/david/sync/projects/ops/davidinfra/opswalrus_bundle/pkg_https---github.com-opswalrus-core_version_/file/exists.ops
237
+ # and the @app.pwd is /home/david/sync/projects/ops/davidinfra
238
+ # then this returns opswalrus_bundle/pkg_https---github.com-opswalrus-core_version_/file/exists.ops
239
+ def relative_path_to_app_pwd
240
+ @ops_file_path.relative_path_from(@app.pwd)
241
+ end
242
+
236
243
  # "/home/david/sync/projects/ops/ops/core/host/info.ops" => "/home/david/sync/projects/ops/ops/core/host"
237
244
  def dirname
238
245
  @ops_file_path.dirname
@@ -48,6 +48,7 @@ module OpsWalrus
48
48
 
49
49
 
50
50
  module OpsFileScriptDSL
51
+ # we run the block in the context of the host proxy object, s.t. `self` within the block evaluates to the host proxy object
51
52
  def ssh_noprep(*args, **kwargs, &block)
52
53
  runtime_env = @runtime_env
53
54
 
@@ -112,6 +113,7 @@ module OpsWalrus
112
113
  results
113
114
  end # def ssh
114
115
 
116
+ # we run the block in the context of the host proxy object, s.t. `self` within the block evaluates to the host proxy object
115
117
  def ssh(*args, **kwargs, &block)
116
118
  runtime_env = @runtime_env
117
119
 
@@ -122,7 +124,6 @@ module OpsWalrus
122
124
 
123
125
  results_lock = Thread::Mutex.new
124
126
  results = {}
125
- # bootstrap_shell_script = BootstrapLinuxHostShellScript
126
127
  # on sshkit_hosts do |sshkit_host|
127
128
  SSHKit::Coordinator.new(sshkit_hosts).each(in: kwargs[:in] || :parallel) do |sshkit_host|
128
129
  # in this context, self is an instance of one of the subclasses of SSHKit::Backend::Abstract, e.g. SSHKit::Backend::Netssh
@@ -263,7 +264,7 @@ module OpsWalrus
263
264
  end
264
265
 
265
266
  # returns the tuple: [stdout, stderr, exit_status]
266
- def shell!(desc_or_cmd = nil, cmd = nil, block = nil, input: nil, log_level: nil)
267
+ def shell!(desc_or_cmd = nil, cmd = nil, block = nil, input: nil)
267
268
  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
268
269
 
269
270
  description = desc_or_cmd if cmd || block
@@ -285,7 +286,7 @@ module OpsWalrus
285
286
  end
286
287
  #cmd = Shellwords.escape(cmd)
287
288
 
288
- report_on(@runtime_env.local_hostname, description, cmd, log_level: log_level) do
289
+ report_on(@runtime_env.local_hostname, description, cmd) do
289
290
  if App.instance.dry_run?
290
291
  ["", "", 0]
291
292
  else
@@ -300,19 +301,25 @@ module OpsWalrus
300
301
  end
301
302
  end
302
303
 
303
- def report_on(hostname, description = nil, cmd, log_level: nil)
304
+ def report_on(hostname, description = nil, cmd)
304
305
  cmd_id = Random.uuid.split('-').first
305
306
 
306
307
  output_block = StringIO.open do |io|
307
- io.print Style.blue(hostname)
308
- io.print " | #{Style.magenta(description)}" if description
309
- io.puts
310
- io.print Style.yellow(cmd_id)
311
- io.print Style.green.bold(" > ")
312
- io.puts Style.yellow(cmd)
308
+ if App.instance.info? # this is true if log_level is trace, debug, info
309
+ io.print Style.blue(hostname)
310
+ io.print " | #{Style.magenta(description)}" if description
311
+ io.puts
312
+ io.print Style.yellow(cmd_id)
313
+ io.print Style.green.bold(" > ")
314
+ io.puts Style.yellow(cmd)
315
+ elsif App.instance.warn? && description
316
+ io.print Style.blue(hostname)
317
+ io.print " | #{Style.magenta(description)}" if description
318
+ io.puts
319
+ end
313
320
  io.string
314
321
  end
315
- puts output_block
322
+ puts output_block unless output_block.empty?
316
323
 
317
324
  t1 = Time.now
318
325
  out, err, exit_status = yield
@@ -320,27 +327,37 @@ module OpsWalrus
320
327
  seconds = t2 - t1
321
328
 
322
329
  output_block = StringIO.open do |io|
323
- if App.instance.info? || log_level == :info
324
- io.puts Style.cyan(out)
325
- io.puts Style.red(err)
326
- elsif App.instance.debug? || log_level == :debug
327
- io.puts Style.cyan(out)
328
- io.puts Style.red(err)
329
- elsif App.instance.trace? || log_level == :trace
330
- io.puts Style.cyan(out)
331
- io.puts Style.red(err)
332
- end
333
- io.print Style.yellow(cmd_id)
334
- io.print Style.blue(" | Finished in #{seconds} seconds with exit status ")
335
- if exit_status == 0
336
- io.puts Style.green("#{exit_status} (#{exit_status == 0 ? 'success' : 'failure'})")
337
- else
338
- io.puts Style.red("#{exit_status} (#{exit_status == 0 ? 'success' : 'failure'})")
330
+ if App.instance.info? # this is true if log_level is trace, debug, info
331
+ if App.instance.trace?
332
+ io.puts Style.cyan(out)
333
+ io.puts Style.red(err)
334
+ elsif App.instance.debug?
335
+ io.puts Style.cyan(out)
336
+ io.puts Style.red(err)
337
+ elsif App.instance.info?
338
+ # io.puts Style.cyan(out)
339
+ # io.puts Style.red(err)
340
+ end
341
+ io.print Style.yellow(cmd_id)
342
+ io.print Style.blue(" | Finished in #{seconds} seconds with exit status ")
343
+ if exit_status == 0
344
+ io.puts Style.green("#{exit_status} (success)")
345
+ else
346
+ io.puts Style.red("#{exit_status} (failure)")
347
+ end
348
+ io.puts Style.green("*" * 80)
349
+ elsif App.instance.warn? && description
350
+ io.print Style.blue(" | Finished in #{seconds} seconds with exit status ")
351
+ if exit_status == 0
352
+ io.puts Style.green("#{exit_status} (success)")
353
+ else
354
+ io.puts Style.red("#{exit_status} (failure)")
355
+ end
356
+ io.puts Style.green("*" * 80)
339
357
  end
340
- io.puts Style.green("*" * 80)
341
358
  io.string
342
359
  end
343
- puts output_block
360
+ puts output_block unless output_block.empty?
344
361
 
345
362
  [out, err, exit_status]
346
363
  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.52"
2
+ VERSION = "1.0.54"
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.52
4
+ version: 1.0.54
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
@@ -186,6 +200,7 @@ files:
186
200
  - lib/opswalrus.rb
187
201
  - lib/opswalrus/_bootstrap.ops
188
202
  - lib/opswalrus/_reboot.ops
203
+ - lib/opswalrus/_run_remote.ops
189
204
  - lib/opswalrus/_shell.ops
190
205
  - lib/opswalrus/app.rb
191
206
  - lib/opswalrus/bootstrap.sh