opswalrus 1.0.8 → 1.0.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CNAME +1 -0
- data/Gemfile.lock +1 -1
- data/README.md +29 -16
- data/lib/opswalrus/app.rb +21 -7
- data/lib/opswalrus/bootstrap.sh +5 -0
- data/lib/opswalrus/cli.rb +16 -15
- data/lib/opswalrus/host.rb +115 -71
- data/lib/opswalrus/invocation.rb +446 -0
- data/lib/opswalrus/operation_runner.rb +3 -3
- data/lib/opswalrus/ops_file.rb +71 -32
- data/lib/opswalrus/ops_file_script.rb +55 -473
- data/lib/opswalrus/ops_file_script_dsl.rb +297 -0
- data/lib/opswalrus/patches.rb +1 -0
- data/lib/opswalrus/runtime_environment.rb +40 -9
- data/lib/opswalrus/sshkit_ext.rb +6 -3
- data/lib/opswalrus/version.rb +1 -1
- data/opswalrus.gemspec +1 -1
- metadata +5 -2
@@ -0,0 +1,297 @@
|
|
1
|
+
require 'shellwords'
|
2
|
+
require 'stringio'
|
3
|
+
|
4
|
+
require 'sshkit'
|
5
|
+
require 'sshkit/dsl'
|
6
|
+
|
7
|
+
require_relative 'host'
|
8
|
+
require_relative 'sshkit_ext'
|
9
|
+
require_relative 'walrus_lang'
|
10
|
+
|
11
|
+
# this file contains all of the logic associated with the invocation of the dynamically defined OpsFileScript#_invoke method
|
12
|
+
|
13
|
+
module OpsWalrus
|
14
|
+
|
15
|
+
class ArrayOrHashNavigationProxy
|
16
|
+
def initialize(array_or_hash)
|
17
|
+
@obj = array_or_hash
|
18
|
+
end
|
19
|
+
def [](index, *args, **kwargs, &block)
|
20
|
+
@obj.method(:[]).call(index, *args, **kwargs, &block)
|
21
|
+
end
|
22
|
+
def respond_to_missing?(method, *)
|
23
|
+
@obj.is_a?(Hash) && @obj.respond_to?(method)
|
24
|
+
end
|
25
|
+
def method_missing(name, *args, **kwargs, &block)
|
26
|
+
case @obj
|
27
|
+
when Array
|
28
|
+
@obj.method(name).call(*args, **kwargs, &block)
|
29
|
+
when Hash
|
30
|
+
if @obj.respond_to?(name)
|
31
|
+
@obj.method(name).call(*args, **kwargs, &block)
|
32
|
+
else
|
33
|
+
value = self[name.to_s]
|
34
|
+
case value
|
35
|
+
when Array, Hash
|
36
|
+
ArrayOrHashNavigationProxy.new(value)
|
37
|
+
else
|
38
|
+
value
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
class InvocationParams
|
46
|
+
# params : Hash
|
47
|
+
def initialize(params)
|
48
|
+
@params = params
|
49
|
+
end
|
50
|
+
|
51
|
+
def [](key)
|
52
|
+
key = key.to_s if key.is_a? Symbol
|
53
|
+
@params[key]
|
54
|
+
end
|
55
|
+
|
56
|
+
def dig(*keys)
|
57
|
+
# keys = keys.map {|key| key.is_a?(Integer) ? key : key.to_s }
|
58
|
+
@params.dig(*keys)
|
59
|
+
end
|
60
|
+
|
61
|
+
def method_missing(name, *args, **kwargs, &block)
|
62
|
+
if @params.respond_to?(name)
|
63
|
+
@params.method(name).call(*args, **kwargs, &block)
|
64
|
+
else
|
65
|
+
value = self[name]
|
66
|
+
case value
|
67
|
+
when Array, Hash
|
68
|
+
ArrayOrHashNavigationProxy.new(value)
|
69
|
+
else
|
70
|
+
value
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
module Invocation
|
77
|
+
class Result
|
78
|
+
attr_accessor :value
|
79
|
+
attr_accessor :exit_status
|
80
|
+
def initialize(value, exit_status = 0)
|
81
|
+
@value = value
|
82
|
+
@exit_status = exit_status
|
83
|
+
end
|
84
|
+
def success?
|
85
|
+
!failure?
|
86
|
+
end
|
87
|
+
def failure?
|
88
|
+
!success?
|
89
|
+
end
|
90
|
+
end
|
91
|
+
class Success < Result
|
92
|
+
def initialize(value)
|
93
|
+
super(value, 0)
|
94
|
+
end
|
95
|
+
def success?
|
96
|
+
true
|
97
|
+
end
|
98
|
+
end
|
99
|
+
class Error < Result
|
100
|
+
def initialize(value, exit_status = 1)
|
101
|
+
super(value, exit_status == 0 ? 1 : exit_status)
|
102
|
+
end
|
103
|
+
def failure?
|
104
|
+
true
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
|
110
|
+
# BootstrapLinuxHostShellScript = <<~SCRIPT
|
111
|
+
# #!/usr/bin/env bash
|
112
|
+
# ...
|
113
|
+
# SCRIPT
|
114
|
+
|
115
|
+
module OpsFileScriptDSL
|
116
|
+
def ssh(*args, **kwargs, &block)
|
117
|
+
hosts = inventory(*args, **kwargs).map {|host| host_proxy_class.new(host) }
|
118
|
+
sshkit_hosts = hosts.map(&:sshkit_host)
|
119
|
+
sshkit_host_to_ops_host_map = sshkit_hosts.zip(hosts).to_h
|
120
|
+
runtime_env = @runtime_env
|
121
|
+
local_host = self
|
122
|
+
# bootstrap_shell_script = BootstrapLinuxHostShellScript
|
123
|
+
# on sshkit_hosts do |sshkit_host|
|
124
|
+
SSHKit::Coordinator.new(sshkit_hosts).each(in: kwargs[:in] || :parallel) do |sshkit_host|
|
125
|
+
|
126
|
+
# in this context, self is an instance of one of the subclasses of SSHKit::Backend::Abstract, e.g. SSHKit::Backend::Netssh
|
127
|
+
|
128
|
+
host = sshkit_host_to_ops_host_map[sshkit_host]
|
129
|
+
# puts "#{host.alias} / #{host}:"
|
130
|
+
|
131
|
+
begin
|
132
|
+
host.set_runtime_env(runtime_env)
|
133
|
+
host.set_ssh_session_connection(self) # self is an instance of one of the subclasses of SSHKit::Backend::Abstract, e.g. SSHKit::Backend::Netssh
|
134
|
+
|
135
|
+
# copy over bootstrap shell script
|
136
|
+
# io = StringIO.new(bootstrap_shell_script)
|
137
|
+
io = File.open(__FILE__.to_pathname.dirname.join("bootstrap.sh"))
|
138
|
+
upload_success = host.upload(io, "tmpopsbootstrap.sh")
|
139
|
+
io.close
|
140
|
+
raise Error, "Unable to upload bootstrap shell script to remote host" unless upload_success
|
141
|
+
host.execute(:chmod, "755", "tmpopsbootstrap.sh")
|
142
|
+
host.execute(:sh, "tmpopsbootstrap.sh")
|
143
|
+
|
144
|
+
# copy over ops bundle zip file
|
145
|
+
zip_bundle_path = runtime_env.zip_bundle_path
|
146
|
+
upload_success = host.upload(zip_bundle_path, "tmpops.zip")
|
147
|
+
raise Error, "Unable to upload ops bundle to remote host" unless upload_success
|
148
|
+
|
149
|
+
stdout, stderr, exit_status = host.run_ops(:bundle, "unzip tmpops.zip", in_bundle_root_dir: false)
|
150
|
+
raise Error, "Unable to unzip ops bundle on remote host" unless exit_status == 0
|
151
|
+
tmp_bundle_root_dir = stdout.strip
|
152
|
+
host.set_ssh_session_tmp_bundle_root_dir(tmp_bundle_root_dir)
|
153
|
+
|
154
|
+
# we run the block in the context of the host, s.t. `self` within the block evaluates to `host`
|
155
|
+
retval = host.instance_exec(local_host, &block) # host is passed as the argument to the block
|
156
|
+
|
157
|
+
# puts retval.inspect
|
158
|
+
|
159
|
+
# cleanup
|
160
|
+
if tmp_bundle_root_dir =~ /tmp/ # sanity check the temp path before we blow away something we don't intend
|
161
|
+
host.execute(:rm, "-rf", "tmpopsbootstrap.sh", "tmpops.zip", tmp_bundle_root_dir)
|
162
|
+
else
|
163
|
+
host.execute(:rm, "-rf", "tmpopsbootstrap.sh", "tmpops.zip")
|
164
|
+
end
|
165
|
+
|
166
|
+
retval
|
167
|
+
rescue SSHKit::Command::Failed => e
|
168
|
+
puts "[!] Command failed:"
|
169
|
+
puts e.message
|
170
|
+
rescue Net::SSH::ConnectionTimeout
|
171
|
+
puts "[!] The host '#{host}' not alive!"
|
172
|
+
rescue Net::SSH::Timeout
|
173
|
+
puts "[!] The host '#{host}' disconnected/timeouted unexpectedly!"
|
174
|
+
rescue Errno::ECONNREFUSED
|
175
|
+
puts "[!] Incorrect port #{port} for #{host}"
|
176
|
+
rescue Net::SSH::HostKeyMismatch => e
|
177
|
+
puts "[!] The host fingerprint does not match the last observed fingerprint for #{host}"
|
178
|
+
puts e.message
|
179
|
+
puts "You might try `ssh-keygen -f ~/.ssh/known_hosts -R \"#{host}\"`"
|
180
|
+
rescue Net::SSH::AuthenticationFailed
|
181
|
+
puts "Wrong Password: #{host} | #{user}:#{password}"
|
182
|
+
rescue Net::SSH::Authentication::DisallowedMethod
|
183
|
+
puts "[!] The host '#{host}' doesn't accept password authentication method."
|
184
|
+
rescue Errno::EHOSTUNREACH => e
|
185
|
+
puts "[!] The host '#{host}' is unreachable"
|
186
|
+
rescue => e
|
187
|
+
puts e.class
|
188
|
+
puts e.message
|
189
|
+
# puts e.backtrace.join("\n")
|
190
|
+
ensure
|
191
|
+
host.clear_ssh_session
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
def inventory(*args, **kwargs)
|
197
|
+
tags = args.map(&:to_s)
|
198
|
+
|
199
|
+
kwargs = kwargs.transform_keys(&:to_s)
|
200
|
+
tags.concat(kwargs["tags"]) if kwargs["tags"]
|
201
|
+
|
202
|
+
@runtime_env.app.inventory(tags)
|
203
|
+
end
|
204
|
+
|
205
|
+
def exit(exit_status, message = nil)
|
206
|
+
if message
|
207
|
+
puts message
|
208
|
+
end
|
209
|
+
result = if exit_status == 0
|
210
|
+
Invocation::Success.new(nil)
|
211
|
+
else
|
212
|
+
Invocation::Error.new(nil, exit_status)
|
213
|
+
end
|
214
|
+
throw :exit_now, result
|
215
|
+
end
|
216
|
+
|
217
|
+
def env(*keys)
|
218
|
+
keys = keys.map(&:to_s)
|
219
|
+
if keys.empty?
|
220
|
+
@env
|
221
|
+
else
|
222
|
+
@env.dig(*keys)
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
226
|
+
# currently, import may only be used to import a package that is referenced in the script's package file
|
227
|
+
# I may decide to extend this to work with dynamic package references
|
228
|
+
#
|
229
|
+
# local_package_name is the local package name defined for the package dependency that is attempting to be referenced
|
230
|
+
def import(local_package_name)
|
231
|
+
local_package_name = local_package_name.to_s
|
232
|
+
package_reference = ops_file.package_file&.dependency(local_package_name)
|
233
|
+
raise Error, "Unknown package reference: #{local_package_name}" unless package_reference
|
234
|
+
import_reference = PackageDependencyReference.new(local_package_name, package_reference)
|
235
|
+
# puts "import: #{import_reference.inspect}"
|
236
|
+
@runtime_env.resolve_import_reference(ops_file, import_reference)
|
237
|
+
end
|
238
|
+
|
239
|
+
def params(*keys, default: nil)
|
240
|
+
keys = keys.map(&:to_s)
|
241
|
+
if keys.empty?
|
242
|
+
@params
|
243
|
+
else
|
244
|
+
@params.dig(*keys) || default
|
245
|
+
end
|
246
|
+
end
|
247
|
+
|
248
|
+
# returns the stdout from the command
|
249
|
+
def sh(desc_or_cmd = nil, cmd = nil, input: nil, &block)
|
250
|
+
out, err, status = *shell!(desc_or_cmd, cmd, block, input: input)
|
251
|
+
out
|
252
|
+
end
|
253
|
+
|
254
|
+
# returns the tuple: [stdout, stderr, exit_status]
|
255
|
+
def shell(desc_or_cmd = nil, cmd = nil, input: nil, &block)
|
256
|
+
shell!(desc_or_cmd, cmd, block, input: input)
|
257
|
+
end
|
258
|
+
|
259
|
+
# returns the tuple: [stdout, stderr, exit_status]
|
260
|
+
def shell!(desc_or_cmd = nil, cmd = nil, block = nil, input: nil)
|
261
|
+
# description = nil
|
262
|
+
|
263
|
+
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
|
264
|
+
|
265
|
+
description = desc_or_cmd if cmd || block
|
266
|
+
cmd = block.call if block
|
267
|
+
cmd ||= desc_or_cmd
|
268
|
+
|
269
|
+
cmd = WalrusLang.render(cmd, block.binding) if block && cmd =~ /{{.*}}/
|
270
|
+
|
271
|
+
#cmd = Shellwords.escape(cmd)
|
272
|
+
|
273
|
+
# puts "shell! self: #{self.inspect}"
|
274
|
+
|
275
|
+
if App.instance.report_mode?
|
276
|
+
print "[#{@runtime_env.local_hostname}] "
|
277
|
+
print "#{description}: " if description
|
278
|
+
puts cmd
|
279
|
+
end
|
280
|
+
|
281
|
+
return unless cmd && !cmd.strip.empty?
|
282
|
+
|
283
|
+
sshkit_cmd = @runtime_env.handle_input(input) do |interaction_handler|
|
284
|
+
# self is a Module instance that is serving as the evaluation context in an instance of a subclass of an Invocation; see Invocation#evaluate
|
285
|
+
backend.execute_cmd(cmd, interaction_handler: interaction_handler, verbosity: :info)
|
286
|
+
end
|
287
|
+
|
288
|
+
[sshkit_cmd.full_stdout, sshkit_cmd.full_stderr, sshkit_cmd.exit_status]
|
289
|
+
end
|
290
|
+
|
291
|
+
# def init_brew
|
292
|
+
# execute('eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)"')
|
293
|
+
# end
|
294
|
+
|
295
|
+
end
|
296
|
+
|
297
|
+
end
|
data/lib/opswalrus/patches.rb
CHANGED
@@ -62,6 +62,18 @@ module OpsWalrus
|
|
62
62
|
@symbol_table = {} # "symbol_name" => ops_file_or_child_namespace
|
63
63
|
end
|
64
64
|
|
65
|
+
def to_s(indent = 0)
|
66
|
+
str = "Namespace: #{@dirname.to_s}\n"
|
67
|
+
@symbol_table.each do |k, v|
|
68
|
+
if v.is_a? Namespace
|
69
|
+
str << "#{' ' * (indent)}|- #{k} : #{v.to_s(indent + 1)}\n"
|
70
|
+
else
|
71
|
+
str << "#{' ' * (indent)}|- #{k} : #{v.to_s}\n"
|
72
|
+
end
|
73
|
+
end
|
74
|
+
str
|
75
|
+
end
|
76
|
+
|
65
77
|
def add(symbol_name, ops_file_or_child_namespace)
|
66
78
|
@symbol_table[symbol_name.to_s] = ops_file_or_child_namespace
|
67
79
|
end
|
@@ -70,11 +82,27 @@ module OpsWalrus
|
|
70
82
|
@symbol_table[symbol_name.to_s]
|
71
83
|
end
|
72
84
|
|
85
|
+
# if this namespace contains an OpsFile of the same name as the namespace, e.g. pkg/install/install.ops, then this
|
86
|
+
# method invokes the OpsFile of that same name and returns the result;
|
87
|
+
# otherwise we return this namespace object
|
88
|
+
def _invoke_if_namespace_has_ops_file_of_same_name(*args, **kwargs, &block)
|
89
|
+
resolved_symbol = resolve_symbol(@dirname.basename)
|
90
|
+
if resolved_symbol.is_a? OpsFile
|
91
|
+
params_hash = resolved_symbol.build_params_hash(*args, **kwargs)
|
92
|
+
resolved_symbol.invoke(runtime_env, params_hash)
|
93
|
+
else
|
94
|
+
self
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
73
98
|
def method_missing(name, *args, **kwargs, &block)
|
99
|
+
# puts "method_missing: #{name}"
|
100
|
+
# puts caller
|
74
101
|
resolved_symbol = resolve_symbol(name)
|
75
102
|
case resolved_symbol
|
76
103
|
when Namespace
|
77
104
|
resolved_symbol
|
105
|
+
resolved_symbol._invoke_if_namespace_has_ops_file_of_same_name(*args, **kwargs)
|
78
106
|
when OpsFile
|
79
107
|
params_hash = resolved_symbol.build_params_hash(*args, **kwargs)
|
80
108
|
resolved_symbol.invoke(runtime_env, params_hash)
|
@@ -82,7 +110,7 @@ module OpsWalrus
|
|
82
110
|
end
|
83
111
|
end
|
84
112
|
|
85
|
-
|
113
|
+
# the assumption is that we have a bundle directory with all the packages in it
|
86
114
|
# and the bundle directory is the root directory
|
87
115
|
class LoadPath
|
88
116
|
include Traversable
|
@@ -96,6 +124,17 @@ module OpsWalrus
|
|
96
124
|
@root_namespace = build_symbol_resolution_tree(@dir)
|
97
125
|
@path_map = build_path_map(@root_namespace)
|
98
126
|
|
127
|
+
# puts "*" * 80
|
128
|
+
# puts "load path for #{@dir}"
|
129
|
+
# puts "-" * 80
|
130
|
+
# puts 'root namespace'
|
131
|
+
# puts @root_namespace.to_s
|
132
|
+
# puts "-" * 80
|
133
|
+
# puts 'path map'
|
134
|
+
# @path_map.each do |k,v|
|
135
|
+
# puts "#{k.to_s}: #{v.to_s}"
|
136
|
+
# end
|
137
|
+
|
99
138
|
@dynamic_package_additions_memo = {}
|
100
139
|
end
|
101
140
|
|
@@ -204,20 +243,12 @@ module OpsWalrus
|
|
204
243
|
configure_sshkit
|
205
244
|
end
|
206
245
|
|
207
|
-
# def with_sudo_password(password, &block)
|
208
|
-
# @interaction_handler.with_sudo_password(password, &block)
|
209
|
-
# end
|
210
|
-
|
211
246
|
# input_mapping : Hash[ String | Regex => String ]
|
212
247
|
# sudo_password : String
|
213
248
|
def handle_input(input_mapping, sudo_password = nil, &block)
|
214
249
|
@interaction_handler.with_mapping(input_mapping, sudo_password, &block)
|
215
250
|
end
|
216
251
|
|
217
|
-
# def handle_input_with_sudo_password(input_mapping, password, &block)
|
218
|
-
# @interaction_handler.with_mapping(input_mapping, password, &block)
|
219
|
-
# end
|
220
|
-
|
221
252
|
# configure sshkit globally
|
222
253
|
def configure_sshkit
|
223
254
|
SSHKit.config.use_format :blackhole
|
data/lib/opswalrus/sshkit_ext.rb
CHANGED
@@ -9,10 +9,13 @@ require_relative 'local_pty_backend'
|
|
9
9
|
module SSHKit
|
10
10
|
module Backend
|
11
11
|
class Abstract
|
12
|
+
def execute(*args)
|
13
|
+
options = { raise_on_non_zero_exit: false }.merge(args.extract_options!)
|
14
|
+
create_command_and_execute(args, options).success?
|
15
|
+
end
|
16
|
+
|
12
17
|
def execute_cmd(*args)
|
13
|
-
|
14
|
-
options = { verbosity: :debug, strip: true }.merge(args.extract_options!)
|
15
|
-
# puts "options: #{options.inspect}"
|
18
|
+
options = { verbosity: :debug, strip: true, raise_on_non_zero_exit: false }.merge(args.extract_options!)
|
16
19
|
create_command_and_execute(args, options)
|
17
20
|
end
|
18
21
|
end
|
data/lib/opswalrus/version.rb
CHANGED
data/opswalrus.gemspec
CHANGED
@@ -39,7 +39,7 @@ Gem::Specification.new do |spec|
|
|
39
39
|
|
40
40
|
spec.add_dependency "bcrypt_pbkdf", "~> 1.1"
|
41
41
|
spec.add_dependency "ed25519", "~> 1.3"
|
42
|
-
spec.add_dependency "sshkit", "~> 1.21"
|
42
|
+
spec.add_dependency "sshkit", "~> 1.21" # sshkit uses net-ssh, which depends on bcrypt_pbkdf and ed25519 to dynamically add support for ed25519 if those two gems are present
|
43
43
|
|
44
44
|
# For more information and examples about making a new gem, check out our
|
45
45
|
# guide at: https://bundler.io/guides/creating_gem.html
|
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.10
|
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-21 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: citrus
|
@@ -118,6 +118,7 @@ extensions: []
|
|
118
118
|
extra_rdoc_files: []
|
119
119
|
files:
|
120
120
|
- ".rspec"
|
121
|
+
- CNAME
|
121
122
|
- Dockerfile
|
122
123
|
- Gemfile
|
123
124
|
- Gemfile.lock
|
@@ -135,11 +136,13 @@ files:
|
|
135
136
|
- lib/opswalrus/host.rb
|
136
137
|
- lib/opswalrus/hosts_file.rb
|
137
138
|
- lib/opswalrus/interaction_handlers.rb
|
139
|
+
- lib/opswalrus/invocation.rb
|
138
140
|
- lib/opswalrus/local_non_blocking_backend.rb
|
139
141
|
- lib/opswalrus/local_pty_backend.rb
|
140
142
|
- lib/opswalrus/operation_runner.rb
|
141
143
|
- lib/opswalrus/ops_file.rb
|
142
144
|
- lib/opswalrus/ops_file_script.rb
|
145
|
+
- lib/opswalrus/ops_file_script_dsl.rb
|
143
146
|
- lib/opswalrus/package_file.rb
|
144
147
|
- lib/opswalrus/patches.rb
|
145
148
|
- lib/opswalrus/runtime_environment.rb
|