opswalrus 1.0.49 → 1.0.50

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