opswalrus 1.0.8 → 1.0.9

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