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.
@@ -1,337 +1,30 @@
1
- require 'json'
2
1
  require 'set'
3
- require 'shellwords'
4
- require 'socket'
5
- require 'stringio'
6
-
7
- # require 'ed25519'
8
- require 'sshkit'
9
- require 'sshkit/dsl'
10
-
11
- require_relative 'host'
12
- require_relative 'sshkit_ext'
13
- require_relative 'walrus_lang'
2
+ require_relative 'ops_file_script_dsl'
14
3
 
15
4
  module OpsWalrus
16
- class ArrayOrHashNavigationProxy
17
- def initialize(array_or_hash)
18
- @obj = array_or_hash
19
- end
20
- def [](index, *args, **kwargs, &block)
21
- @obj.method(:[]).call(index, *args, **kwargs, &block)
22
- end
23
- def respond_to_missing?(method, *)
24
- @obj.is_a?(Hash) && @obj.respond_to?(method)
25
- end
26
- def method_missing(name, *args, **kwargs, &block)
27
- case @obj
28
- when Array
29
- @obj.method(name).call(*args, **kwargs, &block)
30
- when Hash
31
- if @obj.respond_to?(name)
32
- @obj.method(name).call(*args, **kwargs, &block)
33
- else
34
- value = self[name.to_s]
35
- case value
36
- when Array, Hash
37
- ArrayOrHashNavigationProxy.new(value)
38
- else
39
- value
40
- end
41
- end
42
- end
43
- end
44
- end
45
-
46
- class Params
47
- # params : Hash
48
- def initialize(params)
49
- @params = params
50
- end
51
-
52
- def [](key)
53
- key = key.to_s if key.is_a? Symbol
54
- @params[key]
55
- end
56
-
57
- def dig(*keys)
58
- # keys = keys.map {|key| key.is_a?(Integer) ? key : key.to_s }
59
- @params.dig(*keys)
60
- end
61
-
62
- def method_missing(name, *args, **kwargs, &block)
63
- if @params.respond_to?(name)
64
- @params.method(name).call(*args, **kwargs, &block)
65
- else
66
- value = self[name]
67
- case value
68
- when Array, Hash
69
- ArrayOrHashNavigationProxy.new(value)
70
- else
71
- value
72
- end
73
- end
74
- end
75
- end
76
-
77
- class Env
78
- # env : Hash
79
- def initialize(env)
80
- @env = env
81
- end
82
-
83
- def [](key)
84
- key = key.to_s if key.is_a? Symbol
85
- @env[key]
86
- end
87
-
88
- def dig(*keys)
89
- keys = keys.map {|key| key.is_a?(Integer) ? key : key.to_s }
90
- @env.dig(*keys)
91
- end
92
-
93
- def method_missing(name, *args, **kwargs, &block)
94
- if @env.respond_to?(name)
95
- @env.method(name).call(*args, **kwargs, &block)
96
- else
97
- value = self[name]
98
- case value
99
- when Array, Hash
100
- ArrayOrHashNavigationProxy.new(value)
101
- else
102
- value
103
- end
104
- end
105
- end
106
- end
107
-
108
- # BootstrapLinuxHostShellScript = <<~SCRIPT
109
- # #!/usr/bin/env bash
110
- # ...
111
- # SCRIPT
112
-
113
- module DSL
114
- def ssh(*args, **kwargs, &block)
115
- host_proxy_class = @ops_file_script.host_proxy_class
116
- hosts = inventory(*args, **kwargs).map {|host| host_proxy_class.new(host) }
117
- sshkit_hosts = hosts.map(&:sshkit_host)
118
- sshkit_host_to_ops_host_map = sshkit_hosts.zip(hosts).to_h
119
- runtime_env = @runtime_env
120
- local_host = self
121
- # bootstrap_shell_script = BootstrapLinuxHostShellScript
122
- # on sshkit_hosts do |sshkit_host|
123
- SSHKit::Coordinator.new(sshkit_hosts).each(in: kwargs[:in] || :parallel) do |sshkit_host|
124
-
125
- # in this context, self is an instance of one of the subclasses of SSHKit::Backend::Abstract, e.g. SSHKit::Backend::Netssh
126
-
127
- host = sshkit_host_to_ops_host_map[sshkit_host]
128
- # puts "#{host.alias} / #{host}:"
129
-
130
- begin
131
- host.set_runtime_env(runtime_env)
132
- host.set_ssh_session_connection(self) # self is an instance of one of the subclasses of SSHKit::Backend::Abstract, e.g. SSHKit::Backend::Netssh
133
-
134
- # copy over bootstrap shell script
135
- # io = StringIO.new(bootstrap_shell_script)
136
- io = File.open(__FILE__.to_pathname.dirname.join("bootstrap.sh"))
137
- upload_success = host.upload(io, "tmpopsbootstrap.sh")
138
- io.close
139
- raise Error, "Unable to upload bootstrap shell script to remote host" unless upload_success
140
- host.execute(:chmod, "755", "tmpopsbootstrap.sh")
141
- host.execute(:sh, "tmpopsbootstrap.sh")
142
-
143
- # copy over ops bundle zip file
144
- zip_bundle_path = runtime_env.zip_bundle_path
145
- upload_success = host.upload(zip_bundle_path, "tmpops.zip")
146
- raise Error, "Unable to upload ops bundle to remote host" unless upload_success
147
-
148
- stdout, stderr, exit_status = host.run_ops(:bundle, "unzip tmpops.zip", in_bundle_root_dir: false)
149
- raise Error, "Unable to unzip ops bundle on remote host" unless exit_status == 0
150
- tmp_bundle_root_dir = stdout.strip
151
- host.set_ssh_session_tmp_bundle_root_dir(tmp_bundle_root_dir)
152
-
153
- # we run the block in the context of the host, s.t. `self` within the block evaluates to `host`
154
- retval = host.instance_exec(local_host, &block) # host is passed as the argument to the block
155
-
156
- puts retval.inspect
157
-
158
- # cleanup
159
- if tmp_bundle_root_dir =~ /tmp/ # sanity check the temp path before we blow away something we don't intend
160
- host.execute(:rm, "-rf", "tmpopsbootstrap.sh", "tmpops.zip", tmp_bundle_root_dir)
161
- else
162
- host.execute(:rm, "-rf", "tmpopsbootstrap.sh", "tmpops.zip")
163
- end
164
-
165
- retval
166
- rescue SSHKit::Command::Failed => e
167
- puts "[!] Command failed:"
168
- puts e.message
169
- rescue Net::SSH::ConnectionTimeout
170
- puts "[!] The host '#{host}' not alive!"
171
- rescue Net::SSH::Timeout
172
- puts "[!] The host '#{host}' disconnected/timeouted unexpectedly!"
173
- rescue Errno::ECONNREFUSED
174
- puts "[!] Incorrect port #{port} for #{host}"
175
- rescue Net::SSH::HostKeyMismatch => e
176
- puts "[!] The host fingerprint does not match the last observed fingerprint for #{host}"
177
- puts e.message
178
- puts "You might try `ssh-keygen -f ~/.ssh/known_hosts -R \"#{host}\"`"
179
- rescue Net::SSH::AuthenticationFailed
180
- puts "Wrong Password: #{host} | #{user}:#{password}"
181
- rescue Net::SSH::Authentication::DisallowedMethod
182
- puts "[!] The host '#{host}' doesn't accept password authentication method."
183
- rescue Errno::EHOSTUNREACH => e
184
- puts "[!] The host '#{host}' is unreachable"
185
- rescue => e
186
- puts e.class
187
- puts e.message
188
- # puts e.backtrace.join("\n")
189
- ensure
190
- host.clear_ssh_session
191
- end
192
- end
193
- end
194
-
195
- def inventory(*args, **kwargs)
196
- tags = args.map(&:to_s)
197
-
198
- kwargs = kwargs.transform_keys(&:to_s)
199
- tags.concat(kwargs["tags"]) if kwargs["tags"]
200
-
201
- @runtime_env.app.inventory(tags)
202
- end
203
-
204
- def exit(exit_status, message = nil)
205
- if message
206
- puts message
207
- end
208
- result = if exit_status == 0
209
- Invocation::Success.new(nil)
210
- else
211
- Invocation::Error.new(nil, exit_status)
212
- end
213
- throw :exit_now, result
214
- end
215
-
216
- def env(*keys)
217
- keys = keys.map(&:to_s)
218
- if keys.empty?
219
- @env
220
- else
221
- @env.dig(*keys)
222
- end
223
- end
224
-
225
- # currently, import may only be used to import a package that is referenced in the script's package file
226
- # I may decide to extend this to work with dynamic package references
227
- #
228
- # local_package_name is the local package name defined for the package dependency that is attempting to be referenced
229
- def import(local_package_name)
230
- local_package_name = local_package_name.to_s
231
- package_reference = @ops_file_script.ops_file.package_file&.dependency(local_package_name)
232
- raise Error, "Unknown package reference: #{local_package_name}" unless package_reference
233
- import_reference = PackageDependencyReference.new(local_package_name, package_reference)
234
- # puts "import: #{import_reference.inspect}"
235
- @runtime_env.resolve_import_reference(@ops_file_script.ops_file, import_reference)
236
- end
237
-
238
- def params(*keys, default: nil)
239
- keys = keys.map(&:to_s)
240
- if keys.empty?
241
- @params
242
- else
243
- @params.dig(*keys) || default
244
- end
245
- end
246
-
247
- # returns the stdout from the command
248
- def sh(desc_or_cmd = nil, cmd = nil, input: nil, &block)
249
- out, err, status = *shell!(desc_or_cmd, cmd, block, input: input)
250
- out
251
- end
252
-
253
- # returns the tuple: [stdout, stderr, exit_status]
254
- def shell(desc_or_cmd = nil, cmd = nil, input: nil, &block)
255
- shell!(desc_or_cmd, cmd, block, input: input)
256
- end
257
-
258
- # returns the tuple: [stdout, stderr, exit_status]
259
- def shell!(desc_or_cmd = nil, cmd = nil, block = nil, input: nil)
260
- # description = nil
261
-
262
- 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
263
-
264
- description = desc_or_cmd if cmd || block
265
- cmd = block.call if block
266
- cmd ||= desc_or_cmd
267
-
268
- cmd = WalrusLang.render(cmd, block.binding) if block && cmd =~ /{{.*}}/
269
-
270
- #cmd = Shellwords.escape(cmd)
271
-
272
- # puts "shell! self: #{self.inspect}"
273
-
274
- print "[#{@runtime_env.local_hostname}] "
275
- print "#{description}: " if description
276
- puts cmd
277
-
278
- return unless cmd && !cmd.strip.empty?
279
-
280
- # sudo_password = @runtime_env.sudo_password
281
- # sudo_password &&= sudo_password.gsub(/\n+$/,'') # remove trailing newlines from sudo_password
282
-
283
- # puts "shell: #{cmd}"
284
- # puts "shell: #{cmd.inspect}"
285
- # puts "sudo_password: #{sudo_password}"
286
-
287
- # sshkit_cmd = SSHKit::Backend::LocalNonBlocking.new {
288
- # sshkit_cmd = SSHKit::Backend::LocalPty.new {
289
- # sshkit_cmd = backend.execute_cmd(cmd, interaction_handler: SudoPasswordMapper.new(sudo_password).interaction_handler, verbosity: :info)
290
- # execute_cmd(cmd, interaction_handler: SudoPromptInteractionHandler.new, verbosity: :info)
291
- # }.run
292
-
293
- sshkit_cmd = @runtime_env.handle_input(input) do |interaction_handler|
294
- backend.execute_cmd(cmd, interaction_handler: interaction_handler, verbosity: :info)
295
- end
296
-
297
- [sshkit_cmd.full_stdout, sshkit_cmd.full_stderr, sshkit_cmd.exit_status]
298
- end
299
-
300
- # def init_brew
301
- # execute('eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)"')
302
- # end
303
-
304
- end
305
5
 
306
6
  class OpsFileScript
307
- attr_accessor :ops_file
308
7
 
309
- def initialize(ops_file, ruby_script)
310
- @ops_file = ops_file
311
- @ruby_script = ruby_script
312
- @invocation_class = define_invocation_class
313
- @host_proxy_class = define_host_proxy_class
314
- end
8
+ def self.define_for(ops_file, ruby_script)
9
+ klass = Class.new(OpsFileScript)
315
10
 
316
- def define_invocation_class
317
- klass = Class.new(Invocation)
11
+ # puts "OpsFileScript.define_for(#{ops_file.to_s}, #{ruby_script.to_s})"
318
12
 
319
13
  methods_defined = Set.new
320
14
 
321
- # define methods for every import in the script
15
+ # define methods for the OpsFile's local_symbol_table: local imports and private lib directory
322
16
  ops_file.local_symbol_table.each do |symbol_name, import_reference|
323
17
  unless methods_defined.include? symbol_name
18
+ # puts "defining method for local symbol table entry: #{symbol_name}"
324
19
  klass.define_method(symbol_name) do |*args, **kwargs, &block|
325
- # puts "0" * 80
326
- # puts "@runtime_env.resolve_import_reference(@ops_file_script.ops_file, #{import_reference.inspect})"
327
- # puts @ops_file_script.ops_file.ops_file_path
328
- # puts symbol_name
329
- namespace_or_ops_file = @runtime_env.resolve_import_reference(@ops_file_script.ops_file, import_reference)
330
- # puts namespace_or_ops_file.inspect
331
- # puts "0" * 80
20
+ # puts "resolving local symbol table entry: #{symbol_name}"
21
+ namespace_or_ops_file = @runtime_env.resolve_import_reference(ops_file, import_reference)
22
+ # puts "namespace_or_ops_file=#{namespace_or_ops_file.to_s}"
23
+
332
24
  case namespace_or_ops_file
333
25
  when Namespace
334
26
  namespace_or_ops_file
27
+ namespace_or_ops_file._invoke_if_namespace_has_ops_file_of_same_name(*args, **kwargs)
335
28
  when OpsFile
336
29
  params_hash = namespace_or_ops_file.build_params_hash(*args, **kwargs)
337
30
  namespace_or_ops_file.invoke(@runtime_env, params_hash)
@@ -342,22 +35,23 @@ module OpsWalrus
342
35
  end
343
36
 
344
37
  # define methods for every Namespace or OpsFile within the namespace that the OpsFile resides within
345
- sibling_symbol_table = Set.new
346
- sibling_symbol_table |= ops_file.dirname.glob("*.ops").map {|ops_file_path| ops_file_path.basename(".ops").to_s } # OpsFiles
347
- sibling_symbol_table |= ops_file.dirname.glob("*").select(&:directory?).map {|dir_path| dir_path.basename.to_s } # Namespaces
348
- sibling_symbol_table.each do |symbol_name|
38
+ sibling_symbol_table_names = Set.new
39
+ sibling_symbol_table_names |= ops_file.dirname.glob("*.ops").map {|ops_file_path| ops_file_path.basename(".ops").to_s } # OpsFiles
40
+ sibling_symbol_table_names |= ops_file.dirname.glob("*").select(&:directory?).map {|dir_path| dir_path.basename.to_s } # Namespaces
41
+ # puts "sibling_symbol_table_names=#{sibling_symbol_table_names}"
42
+ # puts "methods_defined=#{methods_defined}"
43
+ sibling_symbol_table_names.each do |symbol_name|
349
44
  unless methods_defined.include? symbol_name
45
+ # puts "defining method for implicit imports: #{symbol_name}"
350
46
  klass.define_method(symbol_name) do |*args, **kwargs, &block|
351
- # puts "0" * 80
352
- # puts "@runtime_env.resolve_symbol(@ops_file_script.ops_file, #{symbol_name})"
353
- # puts @ops_file_script.ops_file.ops_file_path
354
- # puts symbol_name
355
- namespace_or_ops_file = @runtime_env.resolve_sibling_symbol(@ops_file_script.ops_file, symbol_name)
356
- # puts namespace_or_ops_file.inspect
357
- # puts "0" * 80
47
+ # puts "resolving implicit import: #{symbol_name}"
48
+ namespace_or_ops_file = @runtime_env.resolve_sibling_symbol(ops_file, symbol_name)
49
+ # puts "namespace_or_ops_file=#{namespace_or_ops_file.to_s}"
50
+
358
51
  case namespace_or_ops_file
359
52
  when Namespace
360
53
  namespace_or_ops_file
54
+ namespace_or_ops_file._invoke_if_namespace_has_ops_file_of_same_name(*args, **kwargs)
361
55
  when OpsFile
362
56
  params_hash = namespace_or_ops_file.build_params_hash(*args, **kwargs)
363
57
  namespace_or_ops_file.invoke(@runtime_env, params_hash)
@@ -367,116 +61,38 @@ module OpsWalrus
367
61
  end
368
62
  end
369
63
 
370
- klass
371
- end
372
-
373
- def define_host_proxy_class
374
- klass = Class.new(HostProxy)
375
-
376
- methods_defined = Set.new
377
-
378
- # define methods for every import in the script
379
- ops_file.local_symbol_table.each do |symbol_name, import_reference|
380
- unless methods_defined.include? symbol_name
381
- # puts "1. defining: #{symbol_name}(...)"
382
- klass.define_method(symbol_name) do |*args, **kwargs, &block|
383
- invocation_builder = case import_reference
384
- # we know we're dealing with a package dependency reference, so we want to run an ops file contained within the bundle directory,
385
- # therefore, we want to reference the specified ops file with respect to the bundle dir
386
- when PackageDependencyReference
387
- HostProxyOpsFileInvocationBuilder.new(self, true)
388
-
389
- # we know we're dealing with a directory reference or OpsFile reference outside of the bundle dir, so we want to reference
390
- # the specified ops file with respect to the root directory, and not with respect to the bundle dir
391
- when DirectoryReference, OpsFileReference
392
- HostProxyOpsFileInvocationBuilder.new(self, false)
393
- end
394
-
395
- invocation_builder.send(symbol_name, *args, **kwargs, &block)
396
- end
397
- methods_defined << symbol_name
64
+ # the evaluation context needs to be a module with all of the following:
65
+ # - OpsFileScriptDSL methods
66
+ # - @runtime_env
67
+ # - @params
68
+ # - #host_proxy_class
69
+ # - #backend
70
+ # - #debug?
71
+ # - #verbose?
72
+ # - all the dynamically defined methods in the subclass of Invocation
73
+ invoke_method_definition = <<~INVOKE_METHOD
74
+ def _invoke(runtime_env, params_hash)
75
+ @runtime_env = runtime_env
76
+ @params = InvocationParams.new(params_hash)
77
+ #{ruby_script}
398
78
  end
399
- end
79
+ INVOKE_METHOD
400
80
 
401
- # define methods for every Namespace or OpsFile within the namespace that the OpsFile resides within
402
- sibling_symbol_table = Set.new
403
- sibling_symbol_table |= ops_file.dirname.glob("*.ops").map {|ops_file_path| ops_file_path.basename(".ops").to_s } # OpsFiles
404
- sibling_symbol_table |= ops_file.dirname.glob("*").select(&:directory?).map {|dir_path| dir_path.basename.to_s } # Namespaces
405
- sibling_symbol_table.each do |symbol_name|
406
- unless methods_defined.include? symbol_name
407
- # puts "2. defining: #{symbol_name}(...)"
408
- klass.define_method(symbol_name) do |*args, **kwargs, &block|
409
- invocation_builder = HostProxyOpsFileInvocationBuilder.new(self, false)
410
- invocation_builder.invoke(symbol_name, *args, **kwargs, &block)
411
- end
412
- methods_defined << symbol_name
413
- end
414
- end
81
+ invoke_method_line_count_prior_to_ruby_script_from_ops_file = 3
82
+ klass.module_eval(invoke_method_definition, ops_file.ops_file_path.to_s, ops_file.script_line_offset - invoke_method_line_count_prior_to_ruby_script_from_ops_file)
415
83
 
416
84
  klass
417
85
  end
418
86
 
419
- def host_proxy_class
420
- @host_proxy_class
421
- end
422
-
423
- def script
424
- @ruby_script
425
- end
426
-
427
- def invoke(runtime_env, params_hash)
428
- # Invocation.new(self, runtime_env, params_hash).evaluate
429
- # puts "INVOKE" * 10
430
- # puts runtime_env.inspect
431
- @invocation_class.new(self, runtime_env, params_hash).evaluate
432
- end
433
-
434
- def to_s
435
- @ruby_script
436
- end
437
- end
438
-
439
- # An Invocation object represents a stack frame, and the params_hash represents the
440
- # arguments that the caller has supplied for that stack frame to reference
441
- class Invocation
442
- class Result
443
- attr_accessor :value
444
- attr_accessor :exit_status
445
- def initialize(value, exit_status = 0)
446
- @value = value
447
- @exit_status = exit_status
448
- end
449
- def success?
450
- !failure?
451
- end
452
- def failure?
453
- !success?
454
- end
455
- end
456
- class Success < Result
457
- def initialize(value)
458
- super(value, 0)
459
- end
460
- def success?
461
- true
462
- end
463
- end
464
- class Error < Result
465
- def initialize(value, exit_status = 1)
466
- super(value, exit_status == 0 ? 1 : exit_status)
467
- end
468
- def failure?
469
- true
470
- end
471
- end
472
87
 
88
+ include OpsFileScriptDSL
473
89
 
474
- include DSL
90
+ attr_accessor :ops_file
475
91
 
476
- def initialize(ops_file_script, runtime_env, params_hash)
477
- @ops_file_script = ops_file_script
478
- @runtime_env = runtime_env
479
- @params = Params.new(params_hash)
92
+ def initialize(ops_file, ruby_script)
93
+ @ops_file = ops_file
94
+ @script = ruby_script
95
+ @runtime_env = nil # this is set at the very first line of #_invoke
480
96
  end
481
97
 
482
98
  def backend
@@ -491,52 +107,18 @@ module OpsWalrus
491
107
  @runtime_env.verbose?
492
108
  end
493
109
 
494
- def evaluate
495
- # catch(:exit_now) do
496
- eval(@ops_file_script.script, nil, @ops_file_script.ops_file.ops_file_path.to_s, @ops_file_script.ops_file.script_line_offset)
497
- # end
110
+ def host_proxy_class
111
+ @ops_file.host_proxy_class
498
112
  end
499
113
 
500
- # def evaluate
501
- # ruby_script_return = begin
502
- # catch(:exit_now) do
503
- # eval(@ops_file_script.script)
504
- # end
505
- # rescue => e
506
- # $stderr.puts "Error: Ops script crashed."
507
- # $stderr.puts e.message
508
- # $stderr.puts e.backtrace.join("\n")
509
- # Error.new(e)
510
- # end
511
-
512
- # if ruby_script_return.is_a? Result
513
- # ruby_script_return
514
- # else
515
- # Success.new(ruby_script_return)
516
- # end
517
- # end
518
-
519
- # def method_missing(name, *args, **kwargs, &block)
520
- # puts "1" * 80
521
- # import_reference = @ops_file_script.ops_file.resolve_import(name)
522
- # if import_reference
523
- # puts "2" * 80
524
- # resolved_value = @runtime_env.resolve_import_reference(@ops_file_script.ops_file, import_reference)
525
- # return resolved_value if resolved_value
526
- # end
114
+ # The _invoke method is dynamically defined as part of OpsFileScript.define_for
115
+ def _invoke(runtime_env, params_hash)
116
+ raise "Not implemented in base class."
117
+ end
527
118
 
528
- # puts "3" * 80
529
- # case namespace_or_ops_file = @runtime_env.resolve_symbol(@ops_file_script.ops_file, name.to_s)
530
- # when Namespace
531
- # puts "4" * 80
532
- # namespace_or_ops_file
533
- # when OpsFile
534
- # puts "5" * 80
535
- # namespace_or_ops_file.invoke(@runtime_env, *args, **kwargs, &block)
536
- # else
537
- # raise NoMethodError, "No method named '#{name}'"
538
- # end
539
- # end
119
+ def to_s
120
+ @script
121
+ end
540
122
  end
541
123
 
542
124
  end