opswalrus 1.0.8 → 1.0.10

Sign up to get free protection for your applications and to get access to all the features.
@@ -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