opswalrus 1.0.49 → 1.0.50

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 90e3b3e2c7e0255ef069e30d31e2125c5bf6fd9c22a15e4aa2057f5265aa6873
4
- data.tar.gz: 58346bbbcfe19a98add4a83b8936f75c911155f872cf35ecaf56ce32679c1123
3
+ metadata.gz: 20cc50a607aa6aec146b62eb5dbef09a908c5485f8e0d3dbc39cf5bf9a92895e
4
+ data.tar.gz: b27cdf6497c28f6ced9be489365003bad03ddb02e6c1285e76c45e6a5ee5d52b
5
5
  SHA512:
6
- metadata.gz: bde3099dff40f9f00add304bac22a5cf62521a68ed3c9481d7de9ae28f3d520c4457097c3be60fbb569e6c32de4fdc101e620fd5a2ad832d335b638245dcd93e
7
- data.tar.gz: a1b4a440fd1d9512efe0cdef6110f3849afab3ad3253140dfa253bb30d7fac606a0de6b814f3611e0c063366a9478a2b4fc3ae5e3c962b02d6c76f2dd815dbb2
6
+ metadata.gz: ec22d55f8417db680b98d0651855fd83589f0b2428c26be7279f7a2b62582120f4eafdb473b4df9004f98934508cf46f9686a1a28fe8c2adf83eadc82b2a0a38
7
+ data.tar.gz: 7eb26d1957c2c8115ccaa06b497418f635a4876279c7a4ee5e16eea10546d80303af209a585d4b69ad02f15cca5ce0ea468af86e974947c6283a7998bd8e6426
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- opswalrus (1.0.49)
4
+ opswalrus (1.0.50)
5
5
  bcrypt_pbkdf (~> 1.1)
6
6
  binding_of_caller (~> 1.0)
7
7
  citrus (~> 3.0)
@@ -0,0 +1,9 @@
1
+ params:
2
+ command: string
3
+ ...
4
+ ssh_noprep in: :sequence do
5
+ # ssh_noprep do
6
+ command = params.command
7
+ desc "Running `#{command}` on #{to_s} (alias=#{self.alias})"
8
+ shell(command)
9
+ end
data/lib/opswalrus/app.rb CHANGED
@@ -31,7 +31,7 @@ module OpsWalrus
31
31
  @instance ||= new(*args)
32
32
  end
33
33
 
34
- LOCAL_SUDO_PASSWORD_PROMPT = "[ops] Enter sudo password to run sudo in local environment: "
34
+ LOCAL_SUDO_PASSWORD_PROMPT = "[opswalrus] Please enter sudo password to run sudo in local environment: "
35
35
 
36
36
 
37
37
  attr_reader :local_hostname
@@ -56,7 +56,7 @@ module OpsWalrus
56
56
  @inventory_tag_selections = []
57
57
  @params = nil
58
58
  @pwd = pwd.to_pathname
59
- @bundler = Bundler.new(@pwd)
59
+ @bundler = Bundler.new(self, @pwd)
60
60
  @local_hostname = "localhost"
61
61
  @mode = :report # :report | :script
62
62
  @dry_run = false
@@ -173,7 +173,7 @@ module OpsWalrus
173
173
 
174
174
  def set_pwd(pwd)
175
175
  @pwd = pwd.to_pathname
176
- @bundler = Bundler.new(@pwd)
176
+ @bundler = Bundler.new(self, @pwd)
177
177
  end
178
178
 
179
179
  def pwd
@@ -217,23 +217,46 @@ module OpsWalrus
217
217
 
218
218
  def bootstrap()
219
219
  set_pwd(__FILE__.to_pathname.dirname)
220
- bootstrap_ops_file = OpsFile.new(self, __FILE__.to_pathname.dirname.join("bootstrap.ops"))
220
+ bootstrap_ops_file = OpsFile.new(self, __FILE__.to_pathname.dirname.join("_bootstrap.ops"))
221
221
  op = OperationRunner.new(self, bootstrap_ops_file)
222
222
  op.run([], params_json_hash: @params)
223
223
  end
224
224
 
225
+ def shell(command)
226
+ set_pwd(__FILE__.to_pathname.dirname)
227
+ shell_ops_file = OpsFile.new(self, __FILE__.to_pathname.dirname.join("_shell.ops"))
228
+ op = OperationRunner.new(self, shell_ops_file)
229
+ puts "running #{command}"
230
+ result = op.run([], params_json_hash: {"command" => command})
231
+ puts "result class=#{result.class}"
232
+ exit_status = result.exit_status
233
+ stdout = JSON.pretty_generate(result.value)
234
+ output = if exit_status == 0
235
+ Style.green(stdout)
236
+ else
237
+ Style.red(stdout)
238
+ end
239
+ puts output
240
+ exit_status
241
+ rescue Error => e
242
+ puts "Error: #{e.message}"
243
+ 1
244
+ end
245
+
225
246
  # args is of the form ["github.com/davidkellis/my-package/sub-package1", "operation1", "arg1:val1", "arg2:val2", "arg3:val3"]
226
247
  # if the first argument is the path to a .ops file, then treat it as a local path, and add the containing package
227
248
  # to the load path
228
249
  # otherwise, copy the
229
250
  # returns the exit status code that the script should terminate with
230
- def run(package_operation_and_args)
251
+ def run(package_operation_and_args, update_bundle: false)
231
252
  return 0 if package_operation_and_args.empty?
232
253
 
233
254
  ops_file_path, operation_kv_args, tmp_bundle_root_dir = get_entry_point_ops_file_and_args(package_operation_and_args)
234
255
 
235
256
  ops_file = load_entry_point_ops_file(ops_file_path, tmp_bundle_root_dir)
236
257
 
258
+ bundler.update if update_bundle
259
+
237
260
  debug "Running: #{ops_file.ops_file_path}"
238
261
 
239
262
  op = OperationRunner.new(self, ops_file)
@@ -27,7 +27,7 @@ if [ -x $RTX ]; then
27
27
 
28
28
  # make sure the latest opswalrus gem is installed
29
29
  # todo: figure out how to install this differently, so that test versions will work
30
- gem install opswalrus
30
+ # gem install opswalrus
31
31
  # $GEM_CMD install opswalrus
32
32
  $RTX reshim
33
33
 
@@ -60,6 +60,9 @@ if echo $OS | grep -q 'ubuntu'; then
60
60
  elif echo $OS | grep -q 'fedora'; then
61
61
  sudo dnf groupinstall -y 'Development Tools'
62
62
  sudo dnf -yq install procps-ng curl file git
63
+ elif echo $OS | grep -q 'rocky'; then
64
+ sudo dnf groupinstall -y 'Development Tools'
65
+ sudo dnf -yq install procps-ng curl file git
63
66
  elif echo $OS | grep -q 'arch'; then
64
67
  sudo pacman -Syu --noconfirm --needed base-devel procps-ng curl file git
65
68
  else
@@ -121,6 +124,9 @@ if echo $OS | grep -q 'ubuntu'; then
121
124
  elif echo $OS | grep -q 'fedora'; then
122
125
  # from https://github.com/rbenv/ruby-build/wiki#suggested-build-environment
123
126
  sudo yum install -y gcc patch bzip2 openssl-devel libyaml-devel libffi-devel readline-devel zlib-devel gdbm-devel ncurses-devel
127
+ elif echo $OS | grep -q 'rocky'; then
128
+ sudo yum --enablerepo=powertools install -y libyaml-devel libffi-devel
129
+ sudo yum install -y gcc patch bzip2 openssl-devel libyaml-devel libffi-devel readline-devel zlib-devel gdbm-devel ncurses-devel
124
130
  elif echo $OS | grep -q 'arch'; then
125
131
  # from https://github.com/rbenv/ruby-build/wiki#suggested-build-environment
126
132
  sudo pacman -Syu --noconfirm --needed base-devel rust libffi libyaml openssl zlib
@@ -150,6 +156,9 @@ if echo $OS | grep -q 'ubuntu'; then
150
156
  sudo needrestart -q -r a
151
157
  elif echo $OS | grep -q 'fedora'; then
152
158
  sudo dnf -yq install age
159
+ elif echo $OS | grep -q 'rocky'; then
160
+ sudo curl -o /usr/local/bin/age https://dl.filippo.io/age/latest?for=linux/amd64
161
+ sudo chmod 755 /usr/local/bin/age
153
162
  elif echo $OS | grep -q 'arch'; then
154
163
  sudo pacman -Syu --noconfirm --needed age
155
164
  else
@@ -11,9 +11,11 @@ module OpsWalrus
11
11
 
12
12
  include Traversable
13
13
 
14
+ attr_accessor :app
14
15
  attr_accessor :pwd
15
16
 
16
- def initialize(working_directory_path)
17
+ def initialize(app, working_directory_path)
18
+ @app = app
17
19
  @pwd = working_directory_path.to_pathname
18
20
  @bundle_dir = @pwd.join(BUNDLE_DIR)
19
21
  end
@@ -53,20 +55,25 @@ module OpsWalrus
53
55
  # bundler_for_package.include_directory_in_bundle_as_self_pkg(pwd)
54
56
  # end
55
57
 
56
- def update
58
+ def update()
57
59
  delete_pwd_bundle_directory
58
60
  ensure_pwd_bundle_directory_exists
59
61
 
60
62
  package_yaml_files = pwd.glob("./**/package.yaml") - pwd.glob("./**/#{BUNDLE_DIR}/**/package.yaml")
61
63
  package_files_within_pwd = package_yaml_files.map {|path| PackageFile.new(path.realpath) }
62
64
 
63
- download_dependency_tree(*package_files_within_pwd)
65
+ download_package_dependency_tree(package_files_within_pwd)
66
+
67
+ ops_files = pwd.glob("./**/*.ops") - pwd.glob("./**/#{BUNDLE_DIR}/**/*.ops")
68
+ ops_files_within_pwd = ops_files.map {|path| OpsFile.new(@app, path.realpath) }
69
+
70
+ download_import_dependencies(ops_files_within_pwd)
64
71
  end
65
72
 
66
- # downloads all transitive package dependencies associated with ops_files
73
+ # downloads all transitive package dependencies associated with ops_files_and_package_files
67
74
  # all downloaded packages are placed into @bundle_dir
68
- def download_dependency_tree(*ops_files_and_package_files)
69
- package_files = ops_files_and_package_files.map(&:package_file).compact.uniq
75
+ def download_package_dependency_tree(*ops_files_and_package_files)
76
+ package_files = ops_files_and_package_files.flatten.map(&:package_file).compact.uniq
70
77
 
71
78
  package_files.each do |root_package_file|
72
79
  pre_order_traverse(root_package_file) do |package_file|
@@ -78,11 +85,29 @@ module OpsWalrus
78
85
  end
79
86
  end
80
87
 
88
+ def download_import_dependencies(*ops_files)
89
+ ops_files.flatten.each do |ops_file|
90
+ ops_file.imports.each do |local_name, import_reference|
91
+ case import_reference
92
+ when PackageDependencyReference, DynamicPackageImportReference
93
+ package_reference = import_reference.package_reference
94
+ download_package(ops_file.dirname, ops_file.ops_file_path, package_reference)
95
+ when DirectoryReference
96
+ # noop
97
+ when OpsFileReference
98
+ # noop
99
+ end
100
+ end
101
+ end
102
+ end
103
+
81
104
  # returns the array of the destination directories that the packages that ops_file depend on were downloaded to
82
105
  # e.g. [dir_path1, dir_path2, dir_path3, ...]
83
106
  def download_package_dependencies(package_file)
107
+ containing_directory = package_file.containing_directory
108
+ package_file_source = package_file.package_file_path
84
109
  package_file.dependencies.map do |local_name, package_reference|
85
- download_package(package_file, package_reference)
110
+ download_package(containing_directory, package_file_source, package_reference)
86
111
  end
87
112
  end
88
113
 
@@ -107,7 +132,9 @@ module OpsWalrus
107
132
 
108
133
  # This method downloads a package_url that is a dependency referenced in the specified package_file
109
134
  # returns the destination directory that the package was downloaded to
110
- def download_package(package_file, package_reference)
135
+ #
136
+ # relative_base_path is the relative base path that any relative file paths captured in the package_reference should be evaluated relative to
137
+ def download_package(relative_base_path, source_of_package_reference, package_reference)
111
138
  ensure_pwd_bundle_directory_exists
112
139
 
113
140
  local_name = package_reference.local_name
@@ -116,22 +143,25 @@ module OpsWalrus
116
143
 
117
144
  destination_package_path = @bundle_dir.join(package_reference.import_resolution_dirname)
118
145
 
119
- App.instance.log("Downloading #{package_reference} referenced in #{package_file.package_file_path} to #{destination_package_path}")
146
+ # App.instance.log("Downloading #{package_reference} referenced in #{package_file.package_file_path} to #{destination_package_path}")
147
+ App.instance.log("Downloading #{package_reference} referenced in #{source_of_package_reference} to #{destination_package_path}")
120
148
 
121
149
  # we return early here under the assumption that an already downloaded package/version combo will not
122
150
  # differ if we download it again multiple times to the same location
123
151
  if destination_package_path.exist?
124
- App.instance.log("Skipping #{package_reference} referenced in #{package_file.package_file_path} since it already has been downloaded to #{destination_package_path}")
152
+ App.instance.log("Skipping #{package_reference} referenced in #{source_of_package_reference} since it already has been downloaded to #{destination_package_path}")
125
153
  return destination_package_path
126
154
  end
127
155
  # FileUtils.remove_dir(destination_package_path) if destination_package_path.exist?
128
156
 
129
- download_package_contents(package_file, local_name, package_url, version, destination_package_path)
157
+ # download_package_contents(package_file.containing_directory, local_name, package_url, version, destination_package_path)
158
+ download_package_contents(relative_base_path, local_name, package_url, version, destination_package_path)
130
159
 
131
160
  destination_package_path
132
161
  end
133
162
 
134
- def download_package_contents(package_file, local_name, package_url, version, destination_package_path)
163
+ # relative_base_path is a Pathname
164
+ def download_package_contents(relative_base_path, local_name, package_url, version, destination_package_path)
135
165
  package_path = package_url.to_pathname
136
166
  package_path = package_path.to_s.gsub(/^~/, Dir.home).to_pathname
137
167
  App.instance.trace("download_package_contents #{package_path}")
@@ -148,7 +178,7 @@ module OpsWalrus
148
178
  end
149
179
  end
150
180
  if package_path.relative? # relative path reference
151
- rebased_path = package_file.containing_directory.join(package_path)
181
+ rebased_path = relative_base_path.join(package_path)
152
182
  if rebased_path.exist?
153
183
  return case
154
184
  when rebased_path.directory?
data/lib/opswalrus/cli.rb CHANGED
@@ -131,11 +131,46 @@ module OpsWalrus
131
131
  $app.set_log_level(global_options[:trace] && :trace || global_options[:debug] && :debug || global_options[:verbose] && :info || :warn)
132
132
 
133
133
  hosts = global_options[:hosts]
134
+ $app.set_inventory_hosts(hosts)
135
+
134
136
  tags = global_options[:tags]
137
+ $app.set_inventory_tags(tags)
138
+
139
+ id_files = global_options[:id]
140
+ id_files = OpsWalrus.env_specified_age_ids if id_files.empty?
141
+
142
+ $app.set_identity_files(id_files)
143
+
144
+ dry_run = [:noop, :dryrun, :dry_run].any? {|sym| global_options[sym] || options[sym] }
145
+ $app.dry_run! if dry_run
146
+
147
+ $app.bootstrap()
148
+ end
149
+ end
135
150
 
151
+ desc "Run a shell command on one or more remote hosts"
152
+ long_desc 'Run a shell command on one or more remote hosts'
153
+ command :shell do |c|
154
+ c.switch :pass, desc: "Prompt for a sudo password"
155
+ c.flag [:u, :user], desc: "Specify the user that the operation will run as"
156
+
157
+ # dry run
158
+ c.switch :noop, desc: "Perform a dry run"
159
+ c.switch :dryrun, desc: "Perform a dry run"
160
+ c.switch :dry_run, desc: "Perform a dry run"
161
+
162
+ c.action do |global_options, options, args|
163
+ $app.set_log_level(global_options[:trace] && :trace || global_options[:debug] && :debug || global_options[:verbose] && :info || :warn)
164
+
165
+ hosts = global_options[:hosts]
136
166
  $app.set_inventory_hosts(hosts)
167
+
168
+ tags = global_options[:tags]
137
169
  $app.set_inventory_tags(tags)
138
170
 
171
+ user = options[:user]
172
+ $app.set_sudo_user(user) if user
173
+
139
174
  id_files = global_options[:id]
140
175
  id_files = OpsWalrus.env_specified_age_ids if id_files.empty?
141
176
 
@@ -144,7 +179,13 @@ module OpsWalrus
144
179
  dry_run = [:noop, :dryrun, :dry_run].any? {|sym| global_options[sym] || options[sym] }
145
180
  $app.dry_run! if dry_run
146
181
 
147
- $app.bootstrap()
182
+ if options[:pass]
183
+ $app.prompt_sudo_password
184
+ end
185
+
186
+ exit_status = $app.shell(args.join(" "))
187
+
188
+ exit_now!("error", exit_status) unless exit_status == 0
148
189
  end
149
190
  end
150
191
 
@@ -152,6 +193,7 @@ module OpsWalrus
152
193
  long_desc 'Run the specified operation found within the specified package'
153
194
  arg 'args', :multiple
154
195
  command :run do |c|
196
+ c.switch [:b, :bundle], desc: "Update bundle prior to running the specified operation"
155
197
  c.switch :pass, desc: "Prompt for a sudo password"
156
198
  c.switch :script, desc: "Script mode"
157
199
 
@@ -194,7 +236,7 @@ module OpsWalrus
194
236
  $app.script_mode!
195
237
  end
196
238
 
197
- exit_status = $app.run(args)
239
+ exit_status = $app.run(args, update_bundle: options[:bundle])
198
240
 
199
241
  exit_now!("error", exit_status) unless exit_status == 0
200
242
  end
@@ -30,7 +30,7 @@ module OpsWalrus
30
30
  invocation_context = case import_reference
31
31
  # we know we're dealing with a package dependency reference, so we want to run an ops file contained within the bundle directory,
32
32
  # therefore, we want to reference the specified ops file with respect to the bundle dir
33
- when PackageDependencyReference
33
+ when PackageDependencyReference, DynamicPackageImportReference
34
34
  RemoteImportInvocationContext.new(@runtime_env, self, namespace_or_ops_file, true, ops_prompt_for_sudo_password: !!ssh_password)
35
35
 
36
36
  # we know we're dealing with a directory reference or OpsFile reference outside of the bundle dir, so we want to reference
@@ -261,12 +261,12 @@ module OpsWalrus
261
261
 
262
262
  # cmd = "OPS_GEM=\"#{OPS_GEM}\" OPSWALRUS_LOCAL_HOSTNAME='#{local_hostname_for_remote_host}'; $OPS_GEM exec --conservative -g opswalrus ops"
263
263
  cmd = "OPSWALRUS_LOCAL_HOSTNAME='#{local_hostname_for_remote_host}' eval #{OPS_CMD}"
264
- if App.instance.info?
265
- cmd << " --verbose"
264
+ if App.instance.trace?
265
+ cmd << " --trace"
266
266
  elsif App.instance.debug?
267
267
  cmd << " --debug"
268
- elsif App.instance.trace?
269
- cmd << " --trace"
268
+ elsif App.instance.info?
269
+ cmd << " --verbose"
270
270
  end
271
271
  cmd << " #{ops_command.to_s}"
272
272
  cmd << " #{ops_command_options.to_s}" if ops_command_options
@@ -300,32 +300,56 @@ module OpsWalrus
300
300
  @props[name] || @default_props[name]
301
301
  end
302
302
 
303
+ def ssh_session
304
+ @sshkit_backend
305
+ end
306
+
303
307
  end
304
308
 
305
309
  class Host
306
310
  include HostDSL
307
311
 
308
- def initialize(name_or_ip_or_cidr, tags = [], props = {}, default_props = {}, hosts_file)
309
- @name_or_ip_or_cidr = name_or_ip_or_cidr
312
+ # ssh_uri is a string of the form:
313
+ # - hostname
314
+ # - user@hostname
315
+ # - hostname:port
316
+ # - user@hostname:port
317
+ def initialize(ssh_uri, tags = [], props = {}, default_props = {}, hosts_file = nil)
318
+ @ssh_uri = ssh_uri
319
+ @host = nil
310
320
  @tags = tags.to_set
311
321
  @props = props.is_a?(Array) ? {"tags" => props} : props.to_h
312
322
  @default_props = default_props
313
323
  @hosts_file = hosts_file
314
324
  @tmp_ssh_key_files = []
325
+ parse_ssh_uri!
326
+ end
327
+
328
+ def parse_ssh_uri!
329
+ if match = /^\s*((?<user>.*?)@)?(?<host>.*?)(:(?<port>[0-9]+))?\s*$/.match(@ssh_uri)
330
+ @host ||= match[:host] if match[:host]
331
+ @props["user"] ||= match[:user] if match[:user]
332
+ @props["port"] ||= match[:port].to_i if match[:port]
333
+ end
315
334
  end
316
335
 
317
336
  # secret_ref: SecretRef
318
337
  # returns the decrypted value referenced by the supplied SecretRef
319
338
  def dereference_secret_if_needed(secret_ref)
320
339
  if secret_ref.is_a? SecretRef
340
+ raise "Host #{self} not read from hosts file so no secrets can be dereferenced." unless @hosts_file
321
341
  @hosts_file.read_secret(secret_ref.to_s)
322
342
  else
323
343
  secret_ref
324
344
  end
325
345
  end
326
346
 
347
+ def ssh_uri
348
+ @ssh_uri
349
+ end
350
+
327
351
  def host
328
- @name_or_ip_or_cidr
352
+ @host
329
353
  end
330
354
 
331
355
  def alias
@@ -337,7 +361,7 @@ module OpsWalrus
337
361
  end
338
362
 
339
363
  def ssh_port
340
- @props["port"] || @default_props["port"]
364
+ @props["port"] || @default_props["port"] || 22
341
365
  end
342
366
 
343
367
  def ssh_user
@@ -346,6 +370,9 @@ module OpsWalrus
346
370
 
347
371
  def ssh_password
348
372
  password = @props["password"] || @default_props["password"]
373
+ password ||= begin
374
+ @props["password"] = IO::console.getpass("[opswalrus] Please enter ssh password to connect to #{ssh_user}@#{host}:#{ssh_port}: ")
375
+ end
349
376
  dereference_secret_if_needed(password)
350
377
  end
351
378
 
@@ -354,7 +381,7 @@ module OpsWalrus
354
381
  end
355
382
 
356
383
  def hash
357
- @name_or_ip_or_cidr.hash
384
+ @ssh_uri.hash
358
385
  end
359
386
 
360
387
  def eql?(other)
@@ -362,7 +389,7 @@ module OpsWalrus
362
389
  end
363
390
 
364
391
  def to_s
365
- @name_or_ip_or_cidr
392
+ @ssh_uri
366
393
  end
367
394
 
368
395
  def tag!(*tags)
@@ -396,7 +423,7 @@ module OpsWalrus
396
423
  # the various options for net-ssh are captured in https://net-ssh.github.io/ssh/v1/chapter-2.html
397
424
  @sshkit_host ||= ::SSHKit::Host.new({
398
425
  hostname: host,
399
- port: ssh_port || 22,
426
+ port: ssh_port,
400
427
  user: ssh_user || raise("No ssh user specified to connect to #{host}"),
401
428
  password: ssh_password,
402
429
  keys: keys
@@ -415,6 +442,7 @@ module OpsWalrus
415
442
  if key_file_path_or_in_memory_key_text.is_a? SecretRef # we're dealing with an in-memory key file; we need to write it to a tempfile
416
443
  tempfile = Tempfile.create
417
444
  @tmp_ssh_key_files << tempfile
445
+ raise "Host #{self} not read from hosts file so no secrets can be written." unless @hosts_file
418
446
  key_file_contents = @hosts_file.read_secret(key_file_path_or_in_memory_key_text.to_s)
419
447
  tempfile.write(key_file_contents)
420
448
  tempfile.close # we want to close the file without unlinking so that the editor can write to it
@@ -4,6 +4,7 @@ module OpsWalrus
4
4
 
5
5
  class ScopedMappingInteractionHandler
6
6
  STANDARD_SUDO_PASSWORD_PROMPT = /\[sudo\] password for .*?:\s*/
7
+ STANDARD_SSH_PASSWORD_PROMPT = /.*?@.*?'s password:\s*/
7
8
 
8
9
  attr_accessor :input_mappings # Hash[ String | Regex => String ]
9
10
 
@@ -13,6 +14,14 @@ module OpsWalrus
13
14
  @input_mappings = mapping
14
15
  end
15
16
 
17
+ # sudo_password : String | Nil
18
+ def self.mapping_for_ssh_password_prompt(ssh_password)
19
+ password_response = ssh_password && ::SSHKit::InteractionHandler::Password.new("#{ssh_password}\n")
20
+ {
21
+ STANDARD_SSH_PASSWORD_PROMPT => password_response,
22
+ }
23
+ end
24
+
16
25
  # sudo_password : String | Nil
17
26
  def self.mapping_for_sudo_password(sudo_password)
18
27
  password_response = sudo_password && ::SSHKit::InteractionHandler::Password.new("#{sudo_password}\n")
@@ -51,7 +60,7 @@ module OpsWalrus
51
60
  end
52
61
  new_mapping.merge!(password_mappings) if password_mappings
53
62
 
54
- if new_mapping.empty?
63
+ if new_mapping.empty? || new_mapping == @input_mappings
55
64
  yield self
56
65
  else
57
66
  yield ScopedMappingInteractionHandler.new(new_mapping, @log_level)
@@ -137,9 +137,10 @@ module OpsWalrus
137
137
  json_kwargs_tempfile.close rescue nil
138
138
  File.unlink(json_kwargs_tempfile) rescue nil
139
139
  end
140
- if remote_json_kwargs_tempfile_basename
141
- @host_proxy.execute(:rm, "-f", remote_json_kwargs_tempfile_basename)
142
- end
140
+ # todo: make sure this cleanup is present
141
+ # if remote_json_kwargs_tempfile_basename
142
+ # @host_proxy.execute(:rm, "-f", remote_json_kwargs_tempfile_basename)
143
+ # end
143
144
  end
144
145
  end
145
146
  end
@@ -163,7 +163,7 @@ module OpsWalrus
163
163
  end
164
164
 
165
165
  package_uri = import_str
166
- if Git.repo?(package_uri) # ops file has imported an ad-hoc git repo
166
+ if package_uri = Git.repo?(package_uri) # ops file has imported an ad-hoc git repo
167
167
  destination_package_path = app.bundler.dynamic_package_path_for_git_package(package_uri)
168
168
  App.instance.trace "DynamicPackageImportReference: #{local_name} -> #{destination_package_path}"
169
169
  return DynamicPackageImportReference.new(local_name, DynamicPackageReference.new(local_name, package_uri, nil))
@@ -47,11 +47,6 @@ module OpsWalrus
47
47
  end
48
48
 
49
49
 
50
- # BootstrapLinuxHostShellScript = <<~SCRIPT
51
- # #!/usr/bin/env bash
52
- # ...
53
- # SCRIPT
54
-
55
50
  module OpsFileScriptDSL
56
51
  def ssh_noprep(*args, **kwargs, &block)
57
52
  runtime_env = @runtime_env
@@ -59,48 +54,55 @@ module OpsWalrus
59
54
  hosts = inventory(*args, **kwargs).map {|host| host_proxy_class.new(runtime_env, host) }
60
55
  sshkit_hosts = hosts.map(&:sshkit_host)
61
56
  sshkit_host_to_ops_host_map = sshkit_hosts.zip(hosts).to_h
62
- local_host = self
57
+ ops_file_script = local_host = self
63
58
  # on sshkit_hosts do |sshkit_host|
64
59
  SSHKit::Coordinator.new(sshkit_hosts).each(in: kwargs[:in] || :parallel) do |sshkit_host|
65
60
  # in this context, self is an instance of one of the subclasses of SSHKit::Backend::Abstract, e.g. SSHKit::Backend::Netssh
66
61
  host = sshkit_host_to_ops_host_map[sshkit_host]
67
-
68
- begin
69
- host.set_runtime_env(runtime_env)
70
- host.set_ssh_session_connection(self) # self is an instance of one of the subclasses of SSHKit::Backend::Abstract, e.g. SSHKit::Backend::Netssh
71
-
72
- # we run the block in the context of the host proxy object, s.t. `self` within the block evaluates to the host proxy object
73
- retval = host.instance_exec(local_host, &block) # local_host is passed as the argument to the block
74
-
75
- retval
76
- rescue SSHKit::Command::Failed => e
77
- App.instance.error "[!] Command failed:"
78
- App.instance.error e.message
79
- rescue Net::SSH::ConnectionTimeout
80
- App.instance.error "[!] The host '#{host}' not alive!"
81
- rescue Net::SSH::Timeout
82
- App.instance.error "[!] The host '#{host}' disconnected/timeouted unexpectedly!"
83
- rescue Errno::ECONNREFUSED
84
- App.instance.error "[!] Incorrect port #{port} for #{host}"
85
- rescue Net::SSH::HostKeyMismatch => e
86
- App.instance.error "[!] The host fingerprint does not match the last observed fingerprint for #{host}"
87
- App.instance.error e.message
88
- App.instance.error "You might try `ssh-keygen -f ~/.ssh/known_hosts -R \"#{host}\"`"
89
- rescue Net::SSH::AuthenticationFailed
90
- App.instance.error "Wrong Password: #{host} | #{user}:#{password}"
91
- rescue Net::SSH::Authentication::DisallowedMethod
92
- App.instance.error "[!] The host '#{host}' doesn't accept password authentication method."
93
- rescue Errno::EHOSTUNREACH => e
94
- App.instance.error "[!] The host '#{host}' is unreachable"
95
- rescue => e
96
- App.instance.error e.class
97
- App.instance.error e.message
98
- App.instance.error e.backtrace.join("\n")
99
- ensure
100
- host.clear_ssh_session
101
- end
102
- end
103
- end
62
+ sshkit_backend = self # self is an instance of one of the subclasses of SSHKit::Backend::Abstract, e.g. SSHKit::Backend::Netssh
63
+
64
+ ssh_password_interaction_mapping = host.ssh_password && ScopedMappingInteractionHandler.mapping_for_ssh_password_prompt(host.ssh_password)
65
+ runtime_env.handle_input(ssh_password_interaction_mapping, inherit_existing_mappings: true) do |interaction_handler|
66
+ App.instance.debug("OpsFileScriptDSL#ssh input mappings #{interaction_handler.input_mappings.inspect}")
67
+
68
+ begin
69
+ host.set_runtime_env(runtime_env)
70
+ host.set_ops_file_script(ops_file_script)
71
+ host.set_ssh_session_connection(sshkit_backend)
72
+
73
+ # we run the block in the context of the host proxy object, s.t. `self` within the block evaluates to the host proxy object
74
+ retval = host.instance_exec(local_host, &block) # local_host is passed as the argument to the block
75
+
76
+ retval
77
+ rescue SSHKit::Command::Failed => e
78
+ App.instance.error "[!] Command failed:"
79
+ App.instance.error e.message
80
+ rescue Net::SSH::ConnectionTimeout
81
+ App.instance.error "[!] The host '#{host}' not alive!"
82
+ rescue Net::SSH::Timeout
83
+ App.instance.error "[!] The host '#{host}' disconnected/timeouted unexpectedly!"
84
+ rescue Errno::ECONNREFUSED
85
+ App.instance.error "[!] Incorrect port #{host.ssh_port} for #{host}"
86
+ rescue Net::SSH::HostKeyMismatch => e
87
+ App.instance.error "[!] The host fingerprint does not match the last observed fingerprint for #{host}"
88
+ App.instance.error e.message
89
+ App.instance.error "You might try `ssh-keygen -f ~/.ssh/known_hosts -R \"#{host}\"`"
90
+ rescue Net::SSH::AuthenticationFailed
91
+ App.instance.error "Wrong Password: #{host} | #{host.ssh_user}:#{host.ssh_password}"
92
+ rescue Net::SSH::Authentication::DisallowedMethod
93
+ App.instance.error "[!] The host '#{host}' doesn't accept password authentication method."
94
+ rescue Errno::EHOSTUNREACH => e
95
+ App.instance.error "[!] The host '#{host}' is unreachable"
96
+ rescue => e
97
+ App.instance.error e.class
98
+ App.instance.error e.message
99
+ App.instance.error e.backtrace.join("\n")
100
+ ensure
101
+ host.clear_ssh_session
102
+ end
103
+ end # runtime_env.handle_input
104
+ end # SSHKit::Coordinator
105
+ end # def ssh
104
106
 
105
107
  def ssh(*args, **kwargs, &block)
106
108
  runtime_env = @runtime_env
@@ -114,48 +116,54 @@ module OpsWalrus
114
116
  SSHKit::Coordinator.new(sshkit_hosts).each(in: kwargs[:in] || :parallel) do |sshkit_host|
115
117
  # in this context, self is an instance of one of the subclasses of SSHKit::Backend::Abstract, e.g. SSHKit::Backend::Netssh
116
118
  host = sshkit_host_to_ops_host_map[sshkit_host]
117
-
118
- begin
119
- host.set_runtime_env(runtime_env)
120
- host.set_ops_file_script(ops_file_script)
121
- host.set_ssh_session_connection(self) # self is an instance of one of the subclasses of SSHKit::Backend::Abstract, e.g. SSHKit::Backend::Netssh
122
-
123
- stdout, stderr, exit_status = host._bootstrap_host(false)
124
- retval = if exit_status == 0
125
- host._zip_copy_and_run_ops_bundle(local_host, block)
126
- else
127
- puts "Failed to bootstrap #{host}. Unable to run operation."
119
+ sshkit_backend = self # self is an instance of one of the subclasses of SSHKit::Backend::Abstract, e.g. SSHKit::Backend::Netssh
120
+
121
+ ssh_password_interaction_mapping = host.ssh_password && ScopedMappingInteractionHandler.mapping_for_ssh_password_prompt(host.ssh_password)
122
+ runtime_env.handle_input(ssh_password_interaction_mapping, inherit_existing_mappings: true) do |interaction_handler|
123
+ App.instance.debug("OpsFileScriptDSL#ssh input mappings #{interaction_handler.input_mappings.inspect}")
124
+
125
+ begin
126
+ host.set_runtime_env(runtime_env)
127
+ host.set_ops_file_script(ops_file_script)
128
+ host.set_ssh_session_connection(sshkit_backend)
129
+
130
+ stdout, stderr, exit_status = host._bootstrap_host(true)
131
+ retval = if exit_status == 0
132
+ host._zip_copy_and_run_ops_bundle(local_host, block)
133
+ else
134
+ puts "Failed to bootstrap #{host}. Unable to run operation."
135
+ end
136
+
137
+ retval
138
+ rescue SSHKit::Command::Failed => e
139
+ App.instance.error "[!] Command failed:"
140
+ App.instance.error e.message
141
+ rescue Net::SSH::ConnectionTimeout
142
+ App.instance.error "[!] The host '#{host}' not alive!"
143
+ rescue Net::SSH::Timeout
144
+ App.instance.error "[!] The host '#{host}' disconnected/timeouted unexpectedly!"
145
+ rescue Errno::ECONNREFUSED
146
+ App.instance.error "[!] Incorrect port #{host.ssh_port} for #{host}"
147
+ rescue Net::SSH::HostKeyMismatch => e
148
+ App.instance.error "[!] The host fingerprint does not match the last observed fingerprint for #{host}"
149
+ App.instance.error e.message
150
+ App.instance.error "You might try `ssh-keygen -f ~/.ssh/known_hosts -R \"#{host}\"`"
151
+ rescue Net::SSH::AuthenticationFailed
152
+ App.instance.error "Wrong Password: #{host} | #{host.ssh_user}:#{host.ssh_password}"
153
+ rescue Net::SSH::Authentication::DisallowedMethod
154
+ App.instance.error "[!] The host '#{host}' doesn't accept password authentication method."
155
+ rescue Errno::EHOSTUNREACH => e
156
+ App.instance.error "[!] The host '#{host}' is unreachable"
157
+ rescue => e
158
+ App.instance.error e.class
159
+ App.instance.error e.message
160
+ App.instance.error e.backtrace.join("\n")
161
+ ensure
162
+ host.clear_ssh_session
128
163
  end
129
-
130
- retval
131
- rescue SSHKit::Command::Failed => e
132
- App.instance.error "[!] Command failed:"
133
- App.instance.error e.message
134
- rescue Net::SSH::ConnectionTimeout
135
- App.instance.error "[!] The host '#{host}' not alive!"
136
- rescue Net::SSH::Timeout
137
- App.instance.error "[!] The host '#{host}' disconnected/timeouted unexpectedly!"
138
- rescue Errno::ECONNREFUSED
139
- App.instance.error "[!] Incorrect port #{port} for #{host}"
140
- rescue Net::SSH::HostKeyMismatch => e
141
- App.instance.error "[!] The host fingerprint does not match the last observed fingerprint for #{host}"
142
- App.instance.error e.message
143
- App.instance.error "You might try `ssh-keygen -f ~/.ssh/known_hosts -R \"#{host}\"`"
144
- rescue Net::SSH::AuthenticationFailed
145
- App.instance.error "Wrong Password: #{host} | #{user}:#{password}"
146
- rescue Net::SSH::Authentication::DisallowedMethod
147
- App.instance.error "[!] The host '#{host}' doesn't accept password authentication method."
148
- rescue Errno::EHOSTUNREACH => e
149
- App.instance.error "[!] The host '#{host}' is unreachable"
150
- rescue => e
151
- App.instance.error e.class
152
- App.instance.error e.message
153
- App.instance.error e.backtrace.join("\n")
154
- ensure
155
- host.clear_ssh_session
156
- end
157
- end
158
- end
164
+ end # runtime_env.handle_input
165
+ end # SSHKit::Coordinator
166
+ end # def ssh
159
167
 
160
168
  def current_dir
161
169
  File.dirname(File.realpath(@runtime_ops_file_path)).to_pathname
@@ -72,10 +72,10 @@ module OpsWalrus
72
72
  self
73
73
  end
74
74
 
75
- def bundle!
76
- bundler_for_package = Bundler.new(dirname)
77
- bundler_for_package.update
78
- end
75
+ # def bundle!
76
+ # bundler_for_package = Bundler.new(dirname)
77
+ # bundler_for_package.update
78
+ # end
79
79
 
80
80
  def dirname
81
81
  @package_file_path.dirname
@@ -38,6 +38,10 @@ class String
38
38
  def string!(default: "")
39
39
  self
40
40
  end
41
+
42
+ def integer!(default: 0)
43
+ to_i
44
+ end
41
45
  end
42
46
 
43
47
  class Integer
@@ -48,6 +52,10 @@ class Integer
48
52
  def string!(default: "")
49
53
  to_s
50
54
  end
55
+
56
+ def integer!(default: 0)
57
+ self
58
+ end
51
59
  end
52
60
 
53
61
  class Float
@@ -58,6 +66,10 @@ class Float
58
66
  def string!(default: "")
59
67
  to_s
60
68
  end
69
+
70
+ def integer!(default: 0)
71
+ to_i
72
+ end
61
73
  end
62
74
 
63
75
  class NilClass
@@ -68,6 +80,10 @@ class NilClass
68
80
  def string!(default: "")
69
81
  default
70
82
  end
83
+
84
+ def integer!(default: 0)
85
+ default
86
+ end
71
87
  end
72
88
 
73
89
  class TrueClass
@@ -78,6 +94,10 @@ class TrueClass
78
94
  def string!(default: "")
79
95
  to_s
80
96
  end
97
+
98
+ def integer!(default: 0)
99
+ default
100
+ end
81
101
  end
82
102
 
83
103
  class FalseClass
@@ -88,4 +108,8 @@ class FalseClass
88
108
  def string!(default: "")
89
109
  to_s
90
110
  end
111
+
112
+ def integer!(default: 0)
113
+ default
114
+ end
91
115
  end
@@ -250,6 +250,9 @@ module OpsWalrus
250
250
  # host will be managed by the local ScopedMappingInteractionHandler running within the instance of the ops command
251
251
  # process on the remote host, and the command host will not have any further opportunity to interactively enter
252
252
  # any prompts on the remote host
253
+ # All that to say that this initial mapping handler will be used to fill in the sudo password for commands running
254
+ # on the remote host, and it will be the instance of the ops tool running on the remote host that will interactively
255
+ # fill out the password on the remote host
253
256
  interaction_handler_mapping_for_sudo_password = ScopedMappingInteractionHandler.mapping_for_sudo_password(sudo_password)
254
257
  @interaction_handler = ScopedMappingInteractionHandler.new(interaction_handler_mapping_for_sudo_password)
255
258
 
@@ -1,3 +1,3 @@
1
1
  module OpsWalrus
2
- VERSION = "1.0.49"
2
+ VERSION = "1.0.50"
3
3
  end
@@ -23,8 +23,8 @@ Vagrant.configure("2") do |config|
23
23
  config.vm.provision "shell" do |s|
24
24
  ssh_pub_key = File.readlines("#{Dir.home}/.ssh/id_ops.pub").first.strip
25
25
  s.inline = <<-SHELL
26
- echo #{ssh_pub_key} >> /home/vagrant/.ssh/authorized_keys
27
- echo #{ssh_pub_key} >> /root/.ssh/authorized_keys
26
+ # echo #{ssh_pub_key} >> /home/vagrant/.ssh/authorized_keys
27
+ # echo #{ssh_pub_key} >> /root/.ssh/authorized_keys
28
28
 
29
29
  sudo sed -i 's/PasswordAuthentication no/PasswordAuthentication yes/g' /etc/ssh/sshd_config
30
30
  sudo systemctl restart sshd
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.49
4
+ version: 1.0.50
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-09-14 00:00:00.000000000 Z
11
+ date: 2023-09-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: binding_of_caller
@@ -184,8 +184,9 @@ files:
184
184
  - build.ops
185
185
  - exe/ops
186
186
  - lib/opswalrus.rb
187
+ - lib/opswalrus/_bootstrap.ops
188
+ - lib/opswalrus/_shell.ops
187
189
  - lib/opswalrus/app.rb
188
- - lib/opswalrus/bootstrap.ops
189
190
  - lib/opswalrus/bootstrap.sh
190
191
  - lib/opswalrus/bundler.rb
191
192
  - lib/opswalrus/cli.rb
File without changes