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.
@@ -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
@@ -1,4 +1,5 @@
1
1
  require 'json'
2
+ require 'pathname'
2
3
 
3
4
  class String
4
5
  def escape_single_quotes
@@ -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
- # the assumption is that we have a bundle directory with all the packages in it
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
@@ -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
- # puts "args: #{args.inspect}"
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
@@ -1,3 +1,3 @@
1
1
  module OpsWalrus
2
- VERSION = "1.0.8"
2
+ VERSION = "1.0.10"
3
3
  end
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.8
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-18 00:00:00.000000000 Z
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