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 +4 -4
- data/Gemfile +2 -0
- data/Gemfile.lock +73 -1
- data/lib/opswalrus/_run_remote.ops +11 -0
- data/lib/opswalrus/app.rb +46 -24
- data/lib/opswalrus/bundler.rb +9 -1
- data/lib/opswalrus/cli.rb +25 -30
- data/lib/opswalrus/host.rb +57 -44
- data/lib/opswalrus/interaction_handlers.rb +2 -4
- data/lib/opswalrus/invocation.rb +58 -0
- data/lib/opswalrus/operation_runner.rb +6 -6
- data/lib/opswalrus/ops_file.rb +7 -0
- data/lib/opswalrus/ops_file_script_dsl.rb +46 -29
- data/lib/opswalrus/patches.rb +2 -0
- data/lib/opswalrus/version.rb +1 -1
- data/opswalrus.gemspec +1 -0
- metadata +17 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 50b82ac565a56bb1585653a90c4c0c09b06ba3490c3121da8717e9bdf085f48b
|
4
|
+
data.tar.gz: f38e88fa6db2769cd7ae59291ce28f337c1ca8a9bcd00509f8ccc00cd47b0890
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 751dfb54af8521b4e33a45cd8d38c0aa0b2c433006bd7030e412605584ffb4b7cba44d49fe26b64c7c2c4d0f2cea08a53cd02aea40c1d2341de7d20fbbe1ac35
|
7
|
+
data.tar.gz: a52102d80e0b0f966d7b357173f87cafcb6ddcfdf0e9d0b3aa369e115c5b7da03ef0ce1fb6ba8d25486ac296e2f3e06dc717ab0fe61f96cf3c904b0a14b3d87c
|
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,7 +1,8 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
opswalrus (1.0.
|
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 = :
|
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 = :
|
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
|
-
|
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
|
-
|
228
|
-
op = OperationRunner.new(self,
|
229
|
-
|
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
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
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
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
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
|
-
#
|
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
|
data/lib/opswalrus/bundler.rb
CHANGED
@@ -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
|
34
|
-
switch :
|
35
|
-
switch :
|
33
|
+
switch :loud, desc: "Verbose output"
|
34
|
+
switch :louder, desc: "Debug output"
|
35
|
+
switch :loudest, desc: "Trace output"
|
36
36
|
|
37
|
-
switch :
|
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[:
|
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 :
|
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[:
|
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 = [:
|
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 :
|
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[:
|
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 = [:
|
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 :
|
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[:
|
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 = [:
|
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 :
|
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[:
|
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 = [:
|
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 =
|
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[:
|
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[:
|
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
|
data/lib/opswalrus/host.rb
CHANGED
@@ -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
|
-
|
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,
|
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
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
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?
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
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 << " --
|
329
|
+
cmd << " --loudest"
|
317
330
|
elsif App.instance.debug?
|
318
|
-
cmd << " --
|
331
|
+
cmd << " --louder"
|
319
332
|
elsif App.instance.info?
|
320
|
-
cmd << " --
|
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,
|
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
|
-
|
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
|
64
|
+
yield ScopedMappingInteractionHandler.new(new_mapping)
|
67
65
|
end
|
68
66
|
end
|
69
67
|
|
data/lib/opswalrus/invocation.rb
CHANGED
@@ -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
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
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
|
data/lib/opswalrus/ops_file.rb
CHANGED
@@ -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
|
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
|
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
|
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
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
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?
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
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
|
data/lib/opswalrus/patches.rb
CHANGED
data/lib/opswalrus/version.rb
CHANGED
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.
|
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-
|
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
|