opswalrus 1.0.1 → 1.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile.lock +1 -1
- data/lib/opswalrus/app.rb +64 -27
- data/lib/opswalrus/host.rb +62 -12
- data/lib/opswalrus/interaction_handlers.rb +1 -0
- data/lib/opswalrus/ops_file.rb +2 -2
- data/lib/opswalrus/ops_file_script.rb +66 -14
- data/lib/opswalrus/version.rb +1 -1
- metadata +2 -4
- data/exe/run_ops_bundle +0 -22
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: cf572dd5c13457782bb8256a38f4572ba897a7bb37ab4da0220fd3a69866b8ad
|
4
|
+
data.tar.gz: 6ebc7681b98c3bc745b310eee40f866b8d6d328c37b50552b409ca1362b07889
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2be383a35f09b51d6e42be8280eaf3760d8735aea0b747406e9258b237dc38b55594a56000a88d2a1c398ae7de56d1391903eed0d1c542850f9bd4f167348c9c
|
7
|
+
data.tar.gz: 71f1f58d4dda0533fdbd8fd35612daae8f2ad8390ae215d2d437432b6ab801c9ed87f614eb54c018db3ccbe72806625f38471d16b7edbff72ddfed376530437b
|
data/Gemfile.lock
CHANGED
data/lib/opswalrus/app.rb
CHANGED
@@ -39,6 +39,8 @@ module OpsWalrus
|
|
39
39
|
@instance ||= new(*args)
|
40
40
|
end
|
41
41
|
|
42
|
+
LOCAL_SUDO_PASSWORD_PROMPT = "[ops] Enter sudo password to run sudo in local environment: "
|
43
|
+
|
42
44
|
|
43
45
|
attr_reader :local_hostname
|
44
46
|
|
@@ -119,7 +121,7 @@ module OpsWalrus
|
|
119
121
|
end
|
120
122
|
|
121
123
|
def prompt_sudo_password
|
122
|
-
password = IO::console.getpass(
|
124
|
+
password = IO::console.getpass(LOCAL_SUDO_PASSWORD_PROMPT)
|
123
125
|
set_sudo_password(password)
|
124
126
|
nil
|
125
127
|
end
|
@@ -207,46 +209,52 @@ module OpsWalrus
|
|
207
209
|
tmp_dir = nil
|
208
210
|
|
209
211
|
case
|
212
|
+
when Dir.exist?(package_or_ops_file_reference)
|
213
|
+
dir = package_or_ops_file_reference
|
214
|
+
ops_file_path, operation_kv_args = find_entry_point_ops_file_in_dir(dir, package_operation_and_args)
|
215
|
+
[ops_file_path, operation_kv_args, tmp_dir]
|
210
216
|
when File.exist?(package_or_ops_file_reference)
|
211
217
|
first_filepath = package_or_ops_file_reference.to_pathname.realpath
|
212
|
-
|
218
|
+
|
219
|
+
ops_file_path, operation_kv_args = case first_filepath.extname.downcase
|
213
220
|
when ".ops"
|
214
|
-
first_filepath
|
221
|
+
[first_filepath, package_operation_and_args]
|
215
222
|
when ".zip"
|
216
|
-
tmp_dir = Dir.mktmpdir.to_pathname
|
223
|
+
tmp_dir = Dir.mktmpdir.to_pathname # this is the temporary bundle root dir
|
217
224
|
|
218
225
|
# unzip the bundle into the temp directory
|
219
226
|
DirZipper.unzip(first_filepath, tmp_dir)
|
220
227
|
|
221
|
-
|
222
|
-
|
223
|
-
tmp_dir.join(relative_ops_path)
|
228
|
+
find_entry_point_ops_file_in_dir(tmp_dir, package_operation_and_args)
|
224
229
|
else
|
225
230
|
raise Error, "Unknown file type for entrypoint: #{first_filepath}"
|
226
231
|
end
|
227
|
-
|
228
|
-
|
232
|
+
|
233
|
+
# operation_kv_args = package_operation_and_args
|
234
|
+
[ops_file_path, operation_kv_args, tmp_dir]
|
229
235
|
when repo_url = git_repo?(package_or_ops_file_reference)
|
230
236
|
destination_package_path = bundler.download_git_package(repo_url)
|
231
237
|
|
232
|
-
ops_file_path =
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
end
|
249
|
-
|
238
|
+
ops_file_path, operation_kv_args = find_entry_point_ops_file_in_dir(destination_package_path, package_operation_and_args)
|
239
|
+
|
240
|
+
# ops_file_path = nil
|
241
|
+
# base_path = Pathname.new(destination_package_path)
|
242
|
+
# path_parts = 0
|
243
|
+
# package_operation_and_args.each do |candidate_path_arg|
|
244
|
+
# candidate_base_path = base_path.join(candidate_path_arg)
|
245
|
+
# candidate_ops_file = candidate_base_path.sub_ext(".ops")
|
246
|
+
# if candidate_ops_file.exist?
|
247
|
+
# path_parts += 1
|
248
|
+
# ops_file_path = candidate_ops_file
|
249
|
+
# break
|
250
|
+
# elsif candidate_base_path.exist?
|
251
|
+
# path_parts += 1
|
252
|
+
# else
|
253
|
+
# raise Error, "Operation not found in #{repo_url}: #{candidate_base_path}"
|
254
|
+
# end
|
255
|
+
# base_path = candidate_base_path
|
256
|
+
# end
|
257
|
+
# operation_kv_args = package_operation_and_args.drop(path_parts)
|
250
258
|
|
251
259
|
# for an original package_operation_and_args of ["github.com/davidkellis/my-package", "operation1", "arg1:val1", "arg2:val2", "arg3:val3"]
|
252
260
|
# we return: [ "#{pwd}/#{Bundler::BUNDLE_DIR}/github-com-davidkellis-my-package/operation1.ops", ["arg1:val1", "arg2:val2", "arg3:val3"] ]
|
@@ -256,6 +264,31 @@ module OpsWalrus
|
|
256
264
|
end
|
257
265
|
end
|
258
266
|
|
267
|
+
# returns pair of the form: [ ops_file_path, operation_kv_args ]
|
268
|
+
def find_entry_point_ops_file_in_dir(base_dir, package_operation_and_args)
|
269
|
+
ops_file_path = nil
|
270
|
+
base_path = Pathname.new(base_dir)
|
271
|
+
|
272
|
+
path_parts = 0
|
273
|
+
package_operation_and_args.each do |candidate_path_arg|
|
274
|
+
candidate_base_path = base_path.join(candidate_path_arg)
|
275
|
+
candidate_ops_file = candidate_base_path.sub_ext(".ops")
|
276
|
+
if candidate_ops_file.exist?
|
277
|
+
path_parts += 1
|
278
|
+
ops_file_path = candidate_ops_file
|
279
|
+
break
|
280
|
+
elsif candidate_base_path.exist?
|
281
|
+
path_parts += 1
|
282
|
+
else
|
283
|
+
raise Error, "Operation not found in: #{candidate_base_path}"
|
284
|
+
end
|
285
|
+
base_path = candidate_base_path
|
286
|
+
end
|
287
|
+
operation_kv_args = package_operation_and_args.drop(path_parts)
|
288
|
+
|
289
|
+
[ops_file_path, operation_kv_args]
|
290
|
+
end
|
291
|
+
|
259
292
|
# git_repo?("davidkellis/arborist") -> "https://github.com/davidkellis/arborist"
|
260
293
|
# returns the repo URL
|
261
294
|
def git_repo?(repo_reference)
|
@@ -268,6 +301,10 @@ module OpsWalrus
|
|
268
301
|
working_repo_reference
|
269
302
|
end
|
270
303
|
|
304
|
+
# def is_dir_git_repo?(dir_path)
|
305
|
+
# Git.ls_remote(reference) rescue nil
|
306
|
+
# end
|
307
|
+
|
271
308
|
def bundle_status
|
272
309
|
end
|
273
310
|
|
data/lib/opswalrus/host.rb
CHANGED
@@ -53,18 +53,67 @@ module OpsWalrus
|
|
53
53
|
# execute('eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)"')
|
54
54
|
# end
|
55
55
|
|
56
|
-
|
57
|
-
|
58
|
-
|
56
|
+
# runs the specified ops command with the specified command arguments
|
57
|
+
def run_ops(command, command_arguments, in_bundle_root_dir: true)
|
58
|
+
if in_bundle_root_dir
|
59
|
+
# e.g. /home/linuxbrew/.linuxbrew/bin/gem exec -g opswalrus ops bundle unzip tmpops.zip
|
60
|
+
shell!("/home/linuxbrew/.linuxbrew/bin/gem exec -g opswalrus ops #{command} #{@tmp_bundle_root_dir} #{command_arguments}")
|
61
|
+
else
|
62
|
+
shell!("/home/linuxbrew/.linuxbrew/bin/gem exec -g opswalrus ops #{command} #{command_arguments}")
|
63
|
+
end
|
59
64
|
end
|
60
65
|
|
61
66
|
end
|
62
67
|
|
68
|
+
class HostProxyOpsFileInvocationBuilder
|
69
|
+
def initialize(host_proxy, is_invocation_a_call_to_package_in_bundle_dir = false)
|
70
|
+
@host_proxy = host_proxy
|
71
|
+
@is_invocation_a_call_to_package_in_bundle_dir = is_invocation_a_call_to_package_in_bundle_dir
|
72
|
+
@method_chain = []
|
73
|
+
end
|
74
|
+
|
75
|
+
def method_missing(method_name, *args, **kwargs)
|
76
|
+
@method_chain << method_name.to_s
|
77
|
+
|
78
|
+
if args.empty? && kwargs.empty? # when there are no args and no kwargs, we are just drilling down through another namespace
|
79
|
+
self
|
80
|
+
else
|
81
|
+
# when there are args or kwargs, then the method invocation represents an attempt to run an OpsFile on a remote host,
|
82
|
+
# so we want to build up a command and send it to the remote host via HostDSL#run_ops
|
83
|
+
@method_chain.unshift(Bundler::BUNDLE_DIR) if @is_invocation_a_call_to_package_in_bundle_dir
|
84
|
+
|
85
|
+
# path_to_ops_file_basename = @method_chain.join(File::Separator)
|
86
|
+
# remote_run_command_args << path_to_ops_file_basename
|
87
|
+
|
88
|
+
remote_run_command_args = @method_chain.join(" ")
|
89
|
+
|
90
|
+
unless args.empty?
|
91
|
+
remote_run_command_args << " "
|
92
|
+
remote_run_command_args << args.join(" ")
|
93
|
+
end
|
94
|
+
|
95
|
+
unless kvargs.empty?
|
96
|
+
remote_run_command_args << " "
|
97
|
+
remote_run_command_args << kvargs.map{|k,v| "#{k}=#{v}" }.join(" ") unless kvargs.empty?
|
98
|
+
end
|
99
|
+
|
100
|
+
@host_proxy.run_ops(:run, remote_run_command_args)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
# the subclasses of HostProxy will define methods that handle method dispatch via HostProxyOpsFileInvocationBuilder objects
|
63
106
|
class HostProxy
|
107
|
+
attr_accessor :_host
|
108
|
+
|
64
109
|
def initialize(host)
|
65
|
-
@
|
110
|
+
@_host = host
|
66
111
|
end
|
67
|
-
|
112
|
+
|
113
|
+
# the subclasses of this class will define methods that handle method dispatch via HostProxyOpsFileInvocationBuilder objects
|
114
|
+
|
115
|
+
def method_missing(name, ...)
|
116
|
+
@_host.send(name, ...)
|
68
117
|
end
|
69
118
|
end
|
70
119
|
|
@@ -142,12 +191,17 @@ module OpsWalrus
|
|
142
191
|
})
|
143
192
|
end
|
144
193
|
|
145
|
-
def
|
194
|
+
def set_ssh_session_connection(sshkit_backend)
|
146
195
|
@sshkit_backend = sshkit_backend
|
147
196
|
end
|
148
197
|
|
149
|
-
def
|
150
|
-
@
|
198
|
+
def set_ssh_session_tmp_bundle_root_dir(tmp_bundle_root_dir)
|
199
|
+
@tmp_bundle_root_dir = tmp_bundle_root_dir
|
200
|
+
end
|
201
|
+
|
202
|
+
def clear_ssh_session
|
203
|
+
@sshkit_backend = nil
|
204
|
+
@tmp_bundle_root_dir = nil
|
151
205
|
end
|
152
206
|
|
153
207
|
def execute(*args)
|
@@ -169,10 +223,6 @@ module OpsWalrus
|
|
169
223
|
@sshkit_backend.download!(remote_path.to_s, local_path.to_s)
|
170
224
|
end
|
171
225
|
|
172
|
-
def run
|
173
|
-
|
174
|
-
end
|
175
|
-
|
176
226
|
end
|
177
227
|
|
178
228
|
end
|
data/lib/opswalrus/ops_file.rb
CHANGED
@@ -115,7 +115,7 @@ module OpsWalrus
|
|
115
115
|
end
|
116
116
|
|
117
117
|
def invoke(runtime_env, params_hash)
|
118
|
-
puts "invoking: #{ops_file_path}"
|
118
|
+
# puts "invoking: #{ops_file_path}"
|
119
119
|
script.invoke(runtime_env, params_hash)
|
120
120
|
end
|
121
121
|
|
@@ -146,7 +146,7 @@ module OpsWalrus
|
|
146
146
|
@local_symbol_table ||= begin
|
147
147
|
local_symbol_table = {}
|
148
148
|
|
149
|
-
local_symbol_table.merge(imports)
|
149
|
+
local_symbol_table.merge!(imports)
|
150
150
|
|
151
151
|
# this is the import for the private lib directory if it exists
|
152
152
|
if private_lib_dir.exist?
|
@@ -8,6 +8,7 @@ require 'stringio'
|
|
8
8
|
require 'sshkit'
|
9
9
|
require 'sshkit/dsl'
|
10
10
|
|
11
|
+
require_relative 'host'
|
11
12
|
require_relative 'sshkit_ext'
|
12
13
|
require_relative 'walrus_lang'
|
13
14
|
|
@@ -71,24 +72,24 @@ module OpsWalrus
|
|
71
72
|
end
|
72
73
|
|
73
74
|
class Env
|
74
|
-
#
|
75
|
-
def initialize(
|
76
|
-
@
|
75
|
+
# env : Hash
|
76
|
+
def initialize(env)
|
77
|
+
@env = env
|
77
78
|
end
|
78
79
|
|
79
80
|
def [](key)
|
80
81
|
key = key.to_s if key.is_a? Symbol
|
81
|
-
@
|
82
|
+
@env[key]
|
82
83
|
end
|
83
84
|
|
84
85
|
def dig(*keys)
|
85
86
|
keys = keys.map {|key| key.is_a?(Integer) ? key : key.to_s }
|
86
|
-
@
|
87
|
+
@env.dig(*keys)
|
87
88
|
end
|
88
89
|
|
89
90
|
def method_missing(name, *args, **kwargs, &block)
|
90
|
-
if @
|
91
|
-
@
|
91
|
+
if @env.respond_to?(name)
|
92
|
+
@env.method(name).call(*args, **kwargs, &block)
|
92
93
|
else
|
93
94
|
value = self[name]
|
94
95
|
case value
|
@@ -107,10 +108,9 @@ module OpsWalrus
|
|
107
108
|
# SCRIPT
|
108
109
|
|
109
110
|
module DSL
|
110
|
-
# include SSHKit::DSL
|
111
|
-
|
112
111
|
def ssh(*args, **kwargs, &block)
|
113
|
-
|
112
|
+
host_proxy_class = @ops_file_script.host_proxy_class
|
113
|
+
hosts = inventory(*args, **kwargs).map {|host| host_proxy_class.new(host) }
|
114
114
|
sshkit_hosts = hosts.map(&:sshkit_host)
|
115
115
|
sshkit_host_to_ops_host_map = sshkit_hosts.zip(hosts).to_h
|
116
116
|
runtime_env = @runtime_env
|
@@ -124,7 +124,7 @@ module OpsWalrus
|
|
124
124
|
|
125
125
|
begin
|
126
126
|
# in this context, self is an instance of one of the subclasses of SSHKit::Backend::Abstract, e.g. SSHKit::Backend::Netssh
|
127
|
-
host.
|
127
|
+
host.set_ssh_session_connection(self)
|
128
128
|
|
129
129
|
# copy over bootstrap shell script
|
130
130
|
# io = StringIO.new(bootstrap_shell_script)
|
@@ -140,9 +140,10 @@ module OpsWalrus
|
|
140
140
|
upload_success = host.upload(zip_bundle_path, "tmpops.zip")
|
141
141
|
raise Error, "Unable to upload ops bundle to remote host" unless upload_success
|
142
142
|
|
143
|
-
stdout, stderr, exit_status = host.run_ops(
|
143
|
+
stdout, stderr, exit_status = host.run_ops(:bundle, "unzip tmpops.zip", in_bundle_root_dir: false)
|
144
144
|
raise Error, "Unable to unzip ops bundle on remote host" unless exit_status == 0
|
145
|
-
|
145
|
+
tmp_bundle_root_dir = stdout.strip
|
146
|
+
host.set_ssh_session_tmp_bundle_root_dir(tmp_bundle_root_dir)
|
146
147
|
|
147
148
|
# we run the block in the context of the host, s.t. `self` within the block evaluates to `host`
|
148
149
|
retval = host.instance_exec(local_host, &block) # host is passed as the argument to the block
|
@@ -179,7 +180,7 @@ module OpsWalrus
|
|
179
180
|
puts e.message
|
180
181
|
# puts e.backtrace.join("\n")
|
181
182
|
ensure
|
182
|
-
host.
|
183
|
+
host.clear_ssh_session
|
183
184
|
end
|
184
185
|
end
|
185
186
|
end
|
@@ -291,6 +292,7 @@ module OpsWalrus
|
|
291
292
|
@ops_file = ops_file
|
292
293
|
@ruby_script = ruby_script
|
293
294
|
@invocation_class = define_invocation_class
|
295
|
+
@host_proxy_class = define_host_proxy_class
|
294
296
|
end
|
295
297
|
|
296
298
|
def define_invocation_class
|
@@ -350,6 +352,56 @@ module OpsWalrus
|
|
350
352
|
klass
|
351
353
|
end
|
352
354
|
|
355
|
+
def define_host_proxy_class
|
356
|
+
klass = Class.new(HostProxy)
|
357
|
+
|
358
|
+
methods_defined = Set.new
|
359
|
+
|
360
|
+
# define methods for every import in the script
|
361
|
+
ops_file.local_symbol_table.each do |symbol_name, import_reference|
|
362
|
+
unless methods_defined.include? symbol_name
|
363
|
+
# puts "1. defining: #{symbol_name}(...)"
|
364
|
+
klass.define_method(symbol_name) do |*args, **kwargs, &block|
|
365
|
+
invocation_builder = case import_reference
|
366
|
+
# we know we're dealing with a package dependency reference, so we want to run an ops file contained within the bundle directory,
|
367
|
+
# therefore, we want to reference the specified ops file with respect to the bundle dir
|
368
|
+
when PackageDependencyReference
|
369
|
+
HostProxyOpsFileInvocationBuilder.new(self, true)
|
370
|
+
|
371
|
+
# we know we're dealing with a directory reference or OpsFile reference outside of the bundle dir, so we want to reference
|
372
|
+
# the specified ops file with respect to the root directory, and not with respect to the bundle dir
|
373
|
+
when DirectoryReference, OpsFileReference
|
374
|
+
HostProxyOpsFileInvocationBuilder.new(self, false)
|
375
|
+
end
|
376
|
+
|
377
|
+
invocation_builder.send(symbol_name, *args, **kwargs, &block)
|
378
|
+
end
|
379
|
+
methods_defined << symbol_name
|
380
|
+
end
|
381
|
+
end
|
382
|
+
|
383
|
+
# define methods for every Namespace or OpsFile within the namespace that the OpsFile resides within
|
384
|
+
sibling_symbol_table = Set.new
|
385
|
+
sibling_symbol_table |= ops_file.dirname.glob("*.ops").map {|ops_file_path| ops_file_path.basename(".ops").to_s } # OpsFiles
|
386
|
+
sibling_symbol_table |= ops_file.dirname.glob("*").select(&:directory?).map {|dir_path| dir_path.basename.to_s } # Namespaces
|
387
|
+
sibling_symbol_table.each do |symbol_name|
|
388
|
+
unless methods_defined.include? symbol_name
|
389
|
+
# puts "2. defining: #{symbol_name}(...)"
|
390
|
+
klass.define_method(symbol_name) do |*args, **kwargs, &block|
|
391
|
+
invocation_builder = HostProxyOpsFileInvocationBuilder.new(self, false)
|
392
|
+
invocation_builder.invoke(symbol_name, *args, **kwargs, &block)
|
393
|
+
end
|
394
|
+
methods_defined << symbol_name
|
395
|
+
end
|
396
|
+
end
|
397
|
+
|
398
|
+
klass
|
399
|
+
end
|
400
|
+
|
401
|
+
def host_proxy_class
|
402
|
+
@host_proxy_class
|
403
|
+
end
|
404
|
+
|
353
405
|
def script
|
354
406
|
@ruby_script
|
355
407
|
end
|
data/lib/opswalrus/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: opswalrus
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0.
|
4
|
+
version: 1.0.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- David Ellis
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-08-
|
11
|
+
date: 2023-08-15 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: citrus
|
@@ -114,7 +114,6 @@ email:
|
|
114
114
|
- david@conquerthelawn.com
|
115
115
|
executables:
|
116
116
|
- ops
|
117
|
-
- run_ops_bundle
|
118
117
|
extensions: []
|
119
118
|
extra_rdoc_files: []
|
120
119
|
files:
|
@@ -125,7 +124,6 @@ files:
|
|
125
124
|
- README.md
|
126
125
|
- Rakefile
|
127
126
|
- exe/ops
|
128
|
-
- exe/run_ops_bundle
|
129
127
|
- lib/opswalrus.rb
|
130
128
|
- lib/opswalrus/app.rb
|
131
129
|
- lib/opswalrus/bootstrap.sh
|
data/exe/run_ops_bundle
DELETED
@@ -1,22 +0,0 @@
|
|
1
|
-
#!/usr/bin/env ruby
|
2
|
-
|
3
|
-
require 'opswalrus'
|
4
|
-
|
5
|
-
def main
|
6
|
-
zip_file_path = ARGV.shift.to_pathname
|
7
|
-
|
8
|
-
exit_status = 0
|
9
|
-
|
10
|
-
Dir.mktmpdir do |dir|
|
11
|
-
dir = dir.to_pathname
|
12
|
-
|
13
|
-
# unzip the bundle into the temp directory
|
14
|
-
OpsWalrus::DirZipper.unzip(zip_file_path, dir)
|
15
|
-
|
16
|
-
exit_status = OpsWalrus::Cli.run(ARGV)
|
17
|
-
end
|
18
|
-
|
19
|
-
exit exit_status
|
20
|
-
end
|
21
|
-
|
22
|
-
main
|