opswalrus 1.0.16 → 1.0.17

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: f6d990ac1fac1a918ebca3eac342377ee61702f0864e635e378ad8e69d7ec0d5
4
- data.tar.gz: 900661d04774a42b0a960624fd7fc51b9690c23ad15fba5aaf94f8a3e1241de6
3
+ metadata.gz: f2ce759171a0342644a1e3e2c94555299b093095b1684a69e11142fe29292da4
4
+ data.tar.gz: 75f4cab88e00038c73df02d69718b5cd067f3ed69150c192335894128ef912d0
5
5
  SHA512:
6
- metadata.gz: 88e94b27da26afb8b925f1b363c6ccf889f3e105d28e3f7d2ccfd1d2d8fe1b61d443d64dc1829c42b8644216e19bad1c4e384757246414b62dc18104421cb8fe
7
- data.tar.gz: 3e5b2b2062a592102bed054b7bae779a66b676ca8d47beb42cbab0a6cc6096e191129b3fd9a2c6fc815f8964bf78270b15687cbba99787c0323fb51c87e82c9c
6
+ metadata.gz: 6d47f83ae4dde1c9007f0d4591da7d6d9501218b15dfed542bdf9eda8538cfca14e22baa0d0a9e7dd244ef0db3fc683f862f296e296f713f500a3b084de02dac
7
+ data.tar.gz: 96877f5975e10dd6415194f5f130f3b7b31fcee66199c354cee93386116b4c0e918749c4158ec6da4a9fe64d9414410032c47d5c78c41caf2a36683cab259a03
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- opswalrus (1.0.16)
4
+ opswalrus (1.0.17)
5
5
  amazing_print (~> 1.5)
6
6
  bcrypt_pbkdf (~> 1.1)
7
7
  citrus (~> 3.0)
@@ -9,8 +9,10 @@ PATH
9
9
  git (~> 1.18)
10
10
  gli (~> 2.21)
11
11
  ougai (~> 2.0)
12
+ pastel (~> 0.8)
12
13
  rubyzip (~> 2.3)
13
14
  sshkit (~> 1.21)
15
+ tty-editor (~> 0.7)
14
16
 
15
17
  GEM
16
18
  remote: https://rubygems.org/
@@ -32,6 +34,8 @@ GEM
32
34
  oj (3.16.0)
33
35
  ougai (2.0.0)
34
36
  oj (~> 3.10)
37
+ pastel (0.8.0)
38
+ tty-color (~> 0.5)
35
39
  public_suffix (5.0.3)
36
40
  rake (13.0.6)
37
41
  rchardet (1.8.0)
@@ -52,6 +56,19 @@ GEM
52
56
  sshkit (1.21.5)
53
57
  net-scp (>= 1.1.2)
54
58
  net-ssh (>= 2.8.0)
59
+ tty-color (0.6.0)
60
+ tty-cursor (0.7.1)
61
+ tty-editor (0.7.0)
62
+ tty-prompt (~> 0.22)
63
+ tty-prompt (0.23.1)
64
+ pastel (~> 0.8)
65
+ tty-reader (~> 0.8)
66
+ tty-reader (0.9.0)
67
+ tty-cursor (~> 0.7)
68
+ tty-screen (~> 0.8)
69
+ wisper (~> 2.0)
70
+ tty-screen (0.8.1)
71
+ wisper (2.0.1)
55
72
 
56
73
  PLATFORMS
57
74
  x86_64-linux
data/lib/opswalrus/app.rb CHANGED
@@ -4,11 +4,12 @@ require "json"
4
4
  # require "logger"
5
5
  require "random/formatter"
6
6
  require "ougai"
7
+ require "pastel"
8
+ require "pathname"
7
9
  require "shellwords"
8
10
  require "socket"
9
11
  require "stringio"
10
12
  require "yaml"
11
- require "pathname"
12
13
  require_relative "errors"
13
14
  require_relative "patches"
14
15
  require_relative "git"
@@ -21,6 +22,8 @@ require_relative "version"
21
22
 
22
23
 
23
24
  module OpsWalrus
25
+ Style = Pastel.new(enabled: $stdout.tty?)
26
+
24
27
  class App
25
28
  def self.instance(*args)
26
29
  @instance ||= new(*args)
@@ -32,9 +35,14 @@ module OpsWalrus
32
35
  attr_reader :local_hostname
33
36
 
34
37
  def initialize(pwd = Dir.pwd)
35
- @logger = Ougai::Logger.new($stdout, level: Logger::INFO) # Logger.new($stdout, level: Logger::INFO)
38
+ @logger = Ougai::Logger.new($stdout) # Logger.new($stdout, level: Logger::INFO)
39
+ @logger.level = :info # , :trace or 'trace'
36
40
  @logger.formatter = Ougai::Formatters::Readable.new
37
41
 
42
+ # @logger.warn Style.yellow("warn"), foo: "bar", baz: {qux: "quux"}
43
+ # @logger.info Style.yellow("info"), foo: "bar", baz: {qux: "quux"}
44
+ # @logger.debug Style.yellow("debug"), foo: "bar", baz: {qux: "quux"}
45
+ # @logger.trace Style.yellow("trace"), foo: "bar", baz: {qux: "quux"}
38
46
 
39
47
  @verbose = false
40
48
  @sudo_user = nil
@@ -46,6 +54,7 @@ module OpsWalrus
46
54
  @bundler = Bundler.new(@pwd)
47
55
  @local_hostname = "localhost"
48
56
  @mode = :report # :report | :script
57
+ @dry_run = false
49
58
  end
50
59
 
51
60
  def to_s
@@ -68,6 +77,14 @@ module OpsWalrus
68
77
  @mode == :script
69
78
  end
70
79
 
80
+ def dry_run?
81
+ @dry_run
82
+ end
83
+
84
+ def dry_run!
85
+ @dry_run = true
86
+ end
87
+
71
88
  def set_local_hostname(hostname)
72
89
  hostname = hostname.strip
73
90
  @local_hostname = hostname.empty? ? "localhost" : hostname
@@ -91,37 +108,54 @@ module OpsWalrus
91
108
  @bundler.bundle_dir
92
109
  end
93
110
 
94
- def set_verbose(verbose)
95
- @verbose = verbose
96
- @logger.debug! if verbose?
111
+ # log_level = :fatal, :error, :warn, :info, :debug, :trace
112
+ # irb(main):018:0> Ougai::Logger::TRACE
113
+ # => -1
114
+ # irb(main):019:0> Ougai::Logger::DEBUG
115
+ # => 0
116
+ # irb(main):020:0> Ougai::Logger::INFO
117
+ # => 1
118
+ # irb(main):021:0> Ougai::Logger::WARN
119
+ # => 2
120
+ # irb(main):022:0> Ougai::Logger::ERROR
121
+ # => 3
122
+ # irb(main):023:0> Ougai::Logger::FATAL
123
+ # => 4
124
+ def set_log_level(log_level)
125
+ @logger.level = log_level
97
126
  end
98
127
 
99
128
  def verbose?
100
- @verbose
129
+ @logger.level <= 1
101
130
  end
102
131
 
103
132
  def debug?
104
- @verbose == 2
133
+ @logger.level <= 0
134
+ end
135
+
136
+ def fatal(*args)
137
+ @logger.fatal(*args)
105
138
  end
106
139
 
107
- def error(msg)
108
- @logger.error(msg)
140
+ def error(*args)
141
+ @logger.error(*args)
109
142
  end
110
143
 
111
- def warn(msg)
112
- @logger.warn(msg)
144
+ def warn(*args)
145
+ @logger.warn(*args)
113
146
  end
114
147
 
115
- def log(msg)
116
- @logger.info(msg)
148
+ def log(*args)
149
+ @logger.info(*args)
117
150
  end
151
+ alias_method :info, :log
118
152
 
119
- def debug(msg)
120
- @logger.debug(msg)
153
+ def debug(*args)
154
+ @logger.debug(*args)
121
155
  end
122
156
 
123
- def trace(msg)
124
- @logger.trace(msg)
157
+ def trace(*args)
158
+ @logger.trace(*args)
125
159
  end
126
160
 
127
161
  def set_pwd(pwd)
@@ -9,6 +9,7 @@ if [ -x "$(command -v /home/linuxbrew/.linuxbrew/bin/brew)" ]; then
9
9
  echo 'Ruby is already installed.' >&2
10
10
 
11
11
  # make sure the latest opswalrus gem is installed
12
+ # todo: figure out how to install this differently, so that test versions will work
12
13
  gem install opswalrus
13
14
 
14
15
  exit 0
@@ -26,6 +26,10 @@ module OpsWalrus
26
26
  FileUtils.mkdir_p(@bundle_dir) unless @bundle_dir.exist?
27
27
  end
28
28
 
29
+ def delete_pwd_bundle_directory
30
+ FileUtils.remove_dir(@bundle_dir) if @bundle_dir.exist?
31
+ end
32
+
29
33
  # # returns the OpsFile within the bundle directory that represents the given ops_file (which is outside of the bundle directory)
30
34
  # def build_bundle_for_ops_file(ops_file)
31
35
  # if ops_file.package_file # ops_file is part of larger package
@@ -50,6 +54,7 @@ module OpsWalrus
50
54
  # end
51
55
 
52
56
  def update
57
+ delete_pwd_bundle_directory
53
58
  ensure_pwd_bundle_directory_exists
54
59
 
55
60
  package_yaml_files = pwd.glob("./**/package.yaml") - pwd.glob("./**/#{BUNDLE_DIR}/**/package.yaml")
@@ -110,36 +115,16 @@ module OpsWalrus
110
115
  version = package_reference.version
111
116
 
112
117
  destination_package_path = @bundle_dir.join(package_reference.import_resolution_dirname)
113
- FileUtils.remove_dir(destination_package_path) if destination_package_path.exist?
118
+
119
+ # we return early here under the assumption that an already downloaded package/version combo will not
120
+ # differ if we download it again multiple times to the same location
121
+ if destination_package_path.exist?
122
+ App.instance.log("Skipping #{package_reference} referenced in #{package_file.package_file_path} since it already has been downloaded to #{destination_package_path}")
123
+ return destination_package_path
124
+ end
125
+ # FileUtils.remove_dir(destination_package_path) if destination_package_path.exist?
114
126
 
115
127
  download_package_contents(package_file, local_name, package_url, version, destination_package_path)
116
- # case
117
- # when package_url =~ /\.git/ # git reference
118
- # download_git_package(package_url, version, destination_package_path)
119
- # when package_url.start_with?("file://") # local path
120
- # path = package_url.sub("file://", "")
121
- # path = path.to_pathname
122
- # package_path_to_download = if path.relative? # relative path
123
- # package_file.containing_directory.join(path)
124
- # else # absolute path
125
- # path.realpath
126
- # end
127
-
128
- # raise Error, "Package not found: #{package_path_to_download}" unless package_path_to_download.exist?
129
- # FileUtils.cp_r(package_path_to_download, destination_package_path)
130
- # when package_url.to_pathname.exist? || package_file.containing_directory.join(package_url).exist? # local path
131
- # path = package_url.to_pathname
132
- # package_path_to_download = if path.relative? # relative path
133
- # package_file.containing_directory.join(path)
134
- # else # absolute path
135
- # path.realpath
136
- # end
137
-
138
- # raise Error, "Package not found: #{package_path_to_download}" unless File.exist?(package_path_to_download)
139
- # FileUtils.cp_r(package_path_to_download, destination_package_path)
140
- # else # git reference
141
- # download_git_package(package_url, version, destination_package_path)
142
- # end
143
128
 
144
129
  destination_package_path
145
130
  end
@@ -147,10 +132,12 @@ module OpsWalrus
147
132
  def download_package_contents(package_file, local_name, package_url, version, destination_package_path)
148
133
  package_path = package_url.to_pathname
149
134
  package_path = package_path.to_s.gsub(/^~/, Dir.home).to_pathname
135
+ App.instance.trace("download_package_contents #{package_path}")
150
136
  if package_path.absolute? && package_path.exist? # absolute path reference
151
137
  return case
152
138
  when package_path.directory?
153
139
  package_path_to_download = package_path.realpath
140
+ App.instance.debug("Copying #{package_path_to_download} to #{destination_package_path}")
154
141
  FileUtils.cp_r(package_path_to_download, destination_package_path)
155
142
  when package_path.file?
156
143
  raise Error, "Package reference must be a directory, not a file:: #{local_name}: #{package_path}"
@@ -163,6 +150,7 @@ module OpsWalrus
163
150
  if rebased_path.exist?
164
151
  return case
165
152
  when rebased_path.directory?
153
+ App.instance.debug("Copying #{package_path_to_download} to #{destination_package_path}")
166
154
  package_path_to_download = rebased_path.realpath
167
155
  FileUtils.cp_r(package_path_to_download, destination_package_path)
168
156
  when rebased_path.file?
@@ -174,6 +162,7 @@ module OpsWalrus
174
162
  end
175
163
 
176
164
  if package_uri = Git.repo?(package_url) # git repo
165
+ App.instance.debug("Cloning repo #{package_uri} into #{destination_package_path}")
177
166
  download_git_package(package_uri, version, destination_package_path)
178
167
  end
179
168
  end
@@ -195,15 +184,10 @@ module OpsWalrus
195
184
  end
196
185
 
197
186
  def dynamic_package_path_for_git_package(package_url, version = nil)
198
- package_reference_dirname = sanitize_path(package_url)
187
+ package_reference_dirname = DynamicPackageReference.import_resolution_dirname(package_url, version)
199
188
  bundle_dir.join(package_reference_dirname)
200
189
  end
201
190
 
202
- def sanitize_path(path)
203
- # found this at https://apidock.com/rails/v5.2.3/ActiveStorage/Filename/sanitized
204
- path.encode(Encoding::UTF_8, invalid: :replace, undef: :replace, replace: "�").strip.tr("\u{202E}%$|:;/\t\r\n\\", "-")
205
- end
206
-
207
191
  # returns the directory that the zip file is unzipped into
208
192
  def unzip(zip_bundle_file, output_dir = nil)
209
193
  if zip_bundle_file.to_pathname.exist?
data/lib/opswalrus/cli.rb CHANGED
@@ -9,7 +9,7 @@ module OpsWalrus
9
9
 
10
10
  pre do |global_options, command, options, args|
11
11
  $app = App.instance(Dir.pwd)
12
- $app.set_local_hostname(ENV["WALRUS_LOCAL_HOSTNAME"]) if ENV["WALRUS_LOCAL_HOSTNAME"]
12
+ $app.set_local_hostname(ENV["OPSWALRUS_LOCAL_HOSTNAME"]) if ENV["OPSWALRUS_LOCAL_HOSTNAME"]
13
13
  true
14
14
  end
15
15
 
@@ -31,6 +31,10 @@ module OpsWalrus
31
31
  desc 'Turn on debug mode'
32
32
  switch [:d, :debug]
33
33
 
34
+ switch :noop, desc: "Perform a dry run"
35
+ switch :dryrun, desc: "Perform a dry run"
36
+ switch :dry_run, desc: "Perform a dry run"
37
+
34
38
  flag [:h, :hosts], multiple: true, desc: "Specify the hosts.yaml file"
35
39
  flag [:t, :tags], multiple: true, desc: "Specify a set of tags to filter the hosts by"
36
40
 
@@ -48,7 +52,8 @@ module OpsWalrus
48
52
  hosts = global_options[:hosts]
49
53
  tags = global_options[:tags]
50
54
 
51
- $app.set_verbose(global_options[:debug] || global_options[:verbose])
55
+ log_level = global_options[:debug] && :trace || global_options[:verbose] && :debug || :info
56
+ $app.set_log_level(log_level)
52
57
 
53
58
  $app.report_inventory(hosts, tags: tags)
54
59
  end
@@ -63,28 +68,31 @@ module OpsWalrus
63
68
  c.flag [:p, :params], desc: "JSON string that represents the input parameters for the operation. The JSON string must conform to the params schema for the operation."
64
69
  c.switch :script, desc: "Script mode"
65
70
 
71
+ # dry run
72
+ c.switch :noop, desc: "Perform a dry run"
73
+ c.switch :dryrun, desc: "Perform a dry run"
74
+ c.switch :dry_run, desc: "Perform a dry run"
75
+
66
76
  c.action do |global_options, options, args|
77
+ log_level = global_options[:debug] && :trace || global_options[:verbose] && :debug || :info
78
+ $app.set_log_level(log_level)
79
+
67
80
  hosts = global_options[:hosts] || []
68
81
  tags = global_options[:tags] || []
69
82
 
70
83
  $app.set_inventory_hosts(hosts)
71
84
  $app.set_inventory_tags(tags)
72
85
 
73
- verbose = case
74
- when global_options[:debug]
75
- 2
76
- when global_options[:verbose]
77
- 1
78
- end
79
-
80
86
  user = options[:user]
81
87
  params = options[:params]
82
88
 
83
- $app.set_verbose(verbose)
84
89
  $app.set_params(params)
85
90
 
86
91
  $app.set_sudo_user(user) if user
87
92
 
93
+ dry_run = [:noop, :dryrun, :dry_run].any? {|sym| global_options[sym] || options[sym] }
94
+ $app.dry_run! if dry_run
95
+
88
96
  if options[:pass]
89
97
  $app.prompt_sudo_password
90
98
  end
@@ -107,6 +115,9 @@ module OpsWalrus
107
115
  long_desc 'Download and bundle the latest versions of dependencies for the current package'
108
116
  c.command :update do |update|
109
117
  update.action do |global_options, options, args|
118
+ log_level = global_options[:debug] && :trace || global_options[:verbose] && :debug || :info
119
+ $app.set_log_level(log_level)
120
+
110
121
  $app.bundle_update
111
122
  end
112
123
  end
@@ -125,6 +136,9 @@ module OpsWalrus
125
136
  unzip.flag [:o, :output], desc: "Specify the output directory"
126
137
 
127
138
  unzip.action do |global_options, options, args|
139
+ log_level = global_options[:debug] && :trace || global_options[:verbose] && :debug || :info
140
+ $app.set_log_level(log_level)
141
+
128
142
  output_dir = options[:output]
129
143
  zip_file_path = args.first
130
144
 
@@ -26,12 +26,12 @@ module OpsWalrus
26
26
  # we know we're dealing with a package dependency reference, so we want to run an ops file contained within the bundle directory,
27
27
  # therefore, we want to reference the specified ops file with respect to the bundle dir
28
28
  when PackageDependencyReference
29
- RemoteImportInvocationContext.new(@runtime_env, self, namespace_or_ops_file, true)
29
+ RemoteImportInvocationContext.new(@runtime_env, self, namespace_or_ops_file, true, prompt_for_sudo_password: !!ssh_password)
30
30
 
31
31
  # we know we're dealing with a directory reference or OpsFile reference outside of the bundle dir, so we want to reference
32
32
  # the specified ops file with respect to the root directory, and not with respect to the bundle dir
33
33
  when DirectoryReference, OpsFileReference
34
- RemoteImportInvocationContext.new(@runtime_env, self, namespace_or_ops_file, false)
34
+ RemoteImportInvocationContext.new(@runtime_env, self, namespace_or_ops_file, false, prompt_for_sudo_password: !!ssh_password)
35
35
  end
36
36
 
37
37
  invocation_context._invoke(*args, **kwargs)
@@ -49,7 +49,7 @@ module OpsWalrus
49
49
  unless methods_defined.include? symbol_name
50
50
  # puts "2. defining: #{symbol_name}(...)"
51
51
  klass.define_method(symbol_name) do |*args, **kwargs, &block|
52
- invocation_context = RemoteImportInvocationContext.new(@runtime_env, self, namespace_or_ops_file, false)
52
+ invocation_context = RemoteImportInvocationContext.new(@runtime_env, self, namespace_or_ops_file, false, prompt_for_sudo_password: !!ssh_password)
53
53
  invocation_context._invoke(*args, **kwargs)
54
54
  end
55
55
  methods_defined << symbol_name
@@ -107,13 +107,14 @@ module OpsWalrus
107
107
  #cmd = Shellwords.escape(cmd)
108
108
 
109
109
  if App.instance.report_mode?
110
+ puts Style.green("*" * 80)
110
111
  if self.alias
111
- print "[#{self.alias} | #{host}] "
112
+ print "[#{Style.blue(self.alias)} | #{Style.blue(host)}] "
112
113
  else
113
- print "[#{host}] "
114
+ print "[#{Style.blue(host)}] "
114
115
  end
115
116
  print "#{description}: " if description
116
- puts cmd
117
+ puts Style.yellow(cmd)
117
118
  end
118
119
 
119
120
  return unless cmd && !cmd.strip.empty?
@@ -122,9 +123,12 @@ module OpsWalrus
122
123
  # puts "shell: #{cmd.inspect}"
123
124
  # puts "sudo_password: #{sudo_password}"
124
125
 
125
- sshkit_cmd = execute_cmd(cmd, input: input)
126
-
127
- [sshkit_cmd.full_stdout, sshkit_cmd.full_stderr, sshkit_cmd.exit_status]
126
+ if App.instance.dry_run?
127
+ ["", "", 0]
128
+ else
129
+ sshkit_cmd = execute_cmd(cmd, input: input)
130
+ [sshkit_cmd.full_stdout, sshkit_cmd.full_stderr, sshkit_cmd.exit_status]
131
+ end
128
132
  end
129
133
 
130
134
  # def init_brew
@@ -137,7 +141,13 @@ module OpsWalrus
137
141
  # e.g. /home/linuxbrew/.linuxbrew/bin/gem exec -g opswalrus ops run echo.ops args:foo args:bar
138
142
 
139
143
  # cmd = "/home/linuxbrew/.linuxbrew/bin/gem exec -g opswalrus ops"
140
- cmd = "/home/linuxbrew/.linuxbrew/bin/gem exec -g opswalrus ops"
144
+ local_hostname_for_remote_host = if self.alias
145
+ "#{self.alias} | #{host}"
146
+ else
147
+ host
148
+ end
149
+
150
+ cmd = "OPSWALRUS_LOCAL_HOSTNAME='#{local_hostname_for_remote_host}'; /home/linuxbrew/.linuxbrew/bin/gem exec -g opswalrus ops"
141
151
  cmd << " -v" if verbose
142
152
  cmd << " #{ops_command.to_s}"
143
153
  cmd << " #{ops_command_options.to_s}" if ops_command_options
@@ -5,6 +5,7 @@ module OpsWalrus
5
5
  class ScopedMappingInteractionHandler
6
6
  attr_accessor :input_mappings # Hash[ String | Regex => String ]
7
7
 
8
+ # log_level is one of: :fatal, :error, :warn, :info, :debug, :trace
8
9
  def initialize(mapping, log_level = nil)
9
10
  @log_level = log_level
10
11
  @input_mappings = mapping
@@ -21,11 +22,10 @@ module OpsWalrus
21
22
  # end
22
23
 
23
24
  # sudo_password : String
24
- def mapping_for_sudo_password(sudo_password)
25
+ def self.mapping_for_sudo_password(sudo_password)
25
26
  {
26
27
  /\[sudo\] password for .*?:\s*/ => "#{sudo_password}\n",
27
28
  App::LOCAL_SUDO_PASSWORD_PROMPT => "#{sudo_password}\n",
28
- # /\s+/ => nil, # unnecessary
29
29
  }
30
30
  end
31
31
 
@@ -39,7 +39,7 @@ module OpsWalrus
39
39
  raise ArgumentError.new("mapping must be a Hash") unless mapping.is_a?(Hash)
40
40
 
41
41
  if sudo_password
42
- mapping.merge!(mapping_for_sudo_password(sudo_password))
42
+ mapping.merge!(ScopedMappingInteractionHandler.mapping_for_sudo_password(sudo_password))
43
43
  end
44
44
 
45
45
  if mapping.empty?
@@ -57,17 +57,16 @@ module OpsWalrus
57
57
  end
58
58
 
59
59
  def on_data(_command, stream_name, data, channel)
60
- log("Looking up response for #{stream_name} message #{data.inspect}")
61
-
62
60
  response_data = begin
63
61
  first_matching_key_value_pair = @input_mappings.find {|k, _v| k === data }
64
62
  first_matching_key_value_pair&.last
65
63
  end
66
64
 
67
65
  if response_data.nil?
68
- log("Unable to find interaction handler mapping for #{stream_name}: #{data.inspect} so no response was sent")
66
+ trace(Style.red("No interaction handler mapping for #{stream_name}: #{data} so no response was sent"))
69
67
  else
70
- log("Sending #{response_data.inspect}")
68
+ debug(Style.cyan("Handling #{stream_name} message #{data}"))
69
+ debug(Style.cyan("Sending response #{response_data}"))
71
70
  if channel.respond_to?(:send_data) # Net SSH Channel
72
71
  channel.send_data(response_data)
73
72
  elsif channel.respond_to?(:write) # Local IO
@@ -80,16 +79,21 @@ module OpsWalrus
80
79
 
81
80
  private
82
81
 
83
- def log(message)
84
- # puts message
85
- SSHKit.config.output.send(@log_level, message) unless @log_level.nil?
82
+ def trace(message)
83
+ App.instance.trace(message)
84
+ end
85
+
86
+ def debug(message)
87
+ App.instance.debug(message)
88
+ if [:fatal, :error, :warn, :info, :debug, :trace].include? @log_level
89
+ SSHKit.config.output.send(@log_level, message)
90
+ end
86
91
  end
87
92
 
88
93
  end
89
94
 
90
95
  class PasswdInteractionHandler
91
96
  def on_data(command, stream_name, data, channel)
92
- # puts data
93
97
  case data
94
98
  when '(current) UNIX password: '
95
99
  channel.send_data("old_pw\n")
@@ -118,8 +122,6 @@ module OpsWalrus
118
122
 
119
123
  class SudoPromptInteractionHandler
120
124
  def on_data(command, stream_name, data, channel)
121
- # puts "0" * 80
122
- # puts data.inspect
123
125
  case data
124
126
  when /\[sudo\] password for/
125
127
  if channel.respond_to?(:send_data) # Net::SSH channel
@@ -128,12 +130,11 @@ module OpsWalrus
128
130
  channel.write("conquer\n")
129
131
  end
130
132
  when /\s+/
131
- puts 'space, do nothing'
133
+ nil
132
134
  else
133
135
  raise "Unexpected prompt: #{data} on stream #{stream_name} and channel #{channel.inspect}"
134
- end
136
+ end
135
137
  end
136
138
  end
137
139
 
138
-
139
140
  end
@@ -13,10 +13,18 @@ module OpsWalrus
13
13
  def method_missing(name, *args, **kwargs, &block)
14
14
  raise "Not implemented in base class"
15
15
  end
16
+
17
+ def _bang_method?(name)
18
+ name.to_s.end_with?("!")
19
+ end
20
+
21
+ def _non_bang_method(name)
22
+ name.to_s.sub(/!$/, '')
23
+ end
16
24
  end
17
25
 
18
26
  class RemoteImportInvocationContext < ImportInvocationContext
19
- def initialize(runtime_env, host_proxy, namespace_or_ops_file, is_invocation_a_call_to_package_in_bundle_dir = false)
27
+ def initialize(runtime_env, host_proxy, namespace_or_ops_file, is_invocation_a_call_to_package_in_bundle_dir = false, prompt_for_sudo_password: nil)
20
28
  @runtime_env = runtime_env
21
29
  @host_proxy = host_proxy
22
30
  @initial_namespace_or_ops_file = @namespace_or_ops_file = namespace_or_ops_file
@@ -24,12 +32,46 @@ module OpsWalrus
24
32
 
25
33
  initial_method_name = @namespace_or_ops_file.dirname.basename
26
34
  @method_chain = [initial_method_name]
35
+ @prompt_for_sudo_password = prompt_for_sudo_password
36
+ end
37
+
38
+ def method_missing(name, *args, **kwargs, &block)
39
+ _resolve_method_and_invoke(name, *args, **kwargs)
40
+ end
41
+
42
+ def _resolve_method_and_invoke(name, *args, **kwargs)
43
+ if _bang_method?(name) # foo! is an attempt to invoke the module's default entrypoint
44
+ method_name = _non_bang_method(name)
45
+
46
+ @method_chain << method_name
47
+
48
+ @namespace_or_ops_file = @namespace_or_ops_file.resolve_symbol(method_name)
49
+ _invoke_if_namespace_has_ops_file_of_same_name(*args, **kwargs)
50
+ else
51
+ @method_chain << name.to_s
52
+
53
+ @namespace_or_ops_file = @namespace_or_ops_file.resolve_symbol(name)
54
+ _invoke(*args, **kwargs)
55
+ end
56
+ end
57
+
58
+ # if this namespace contains an OpsFile of the same name as the namespace, e.g. pkg/install/install.ops, then this
59
+ # method invokes the OpsFile of that same name and returns the result;
60
+ # otherwise we return this namespace object
61
+ def _invoke_if_namespace_has_ops_file_of_same_name(*args, **kwargs, &block)
62
+ method_name = @namespace_or_ops_file.dirname.basename
63
+ resolved_symbol = @namespace_or_ops_file.resolve_symbol(method_name)
64
+ if resolved_symbol.is_a? OpsFile
65
+ _resolve_method_and_invoke(method_name)
66
+ else
67
+ self
68
+ end
27
69
  end
28
70
 
29
71
  def _invoke(*args, **kwargs)
30
72
  case @namespace_or_ops_file
31
73
  when Namespace
32
- _invoke_if_namespace_has_ops_file_of_same_name(*args, **kwargs)
74
+ self
33
75
  when OpsFile
34
76
  _invoke_remote(*args, **kwargs)
35
77
  end
@@ -59,32 +101,13 @@ module OpsWalrus
59
101
  end.join(" ")
60
102
  end
61
103
 
62
- @host_proxy.run_ops(:run, "--script", remote_run_command_args)
63
- end
64
-
65
- # if this namespace contains an OpsFile of the same name as the namespace, e.g. pkg/install/install.ops, then this
66
- # method invokes the OpsFile of that same name and returns the result;
67
- # otherwise we return this namespace object
68
- def _invoke_if_namespace_has_ops_file_of_same_name(*args, **kwargs, &block)
69
- method_name = @namespace_or_ops_file.dirname.basename
70
- resolved_symbol = @namespace_or_ops_file.resolve_symbol(method_name)
71
- if resolved_symbol.is_a? OpsFile
72
- _resolve_method_and_invoke(method_name)
104
+ # @host_proxy.run_ops(:run, "--script", remote_run_command_args)
105
+ if @prompt_for_sudo_password
106
+ @host_proxy.run_ops(:run, "--pass", remote_run_command_args)
73
107
  else
74
- self
108
+ @host_proxy.run_ops(:run, remote_run_command_args)
75
109
  end
76
110
  end
77
-
78
- def _resolve_method_and_invoke(name, *args, **kwargs)
79
- @method_chain << name.to_s
80
-
81
- @namespace_or_ops_file = @namespace_or_ops_file.resolve_symbol(name)
82
- _invoke(*args, **kwargs)
83
- end
84
-
85
- def method_missing(name, *args, **kwargs, &block)
86
- _resolve_method_and_invoke(name, *args, **kwargs)
87
- end
88
111
  end
89
112
 
90
113
  class LocalImportInvocationContext < ImportInvocationContext
@@ -93,24 +116,25 @@ module OpsWalrus
93
116
  @initial_namespace_or_ops_file = @namespace_or_ops_file = namespace_or_ops_file
94
117
  end
95
118
 
96
- def _invoke(*args, **kwargs)
97
- case @namespace_or_ops_file
98
- when Namespace
99
- _invoke_if_namespace_has_ops_file_of_same_name(*args, **kwargs)
100
- when OpsFile
101
- _invoke_local(*args, **kwargs)
102
- end
119
+ def method_missing(name, *args, **kwargs, &block)
120
+ _resolve_method_and_invoke(name, *args, **kwargs)
103
121
  end
104
122
 
105
- def _invoke_local(*args, **kwargs)
106
- params_hash = @namespace_or_ops_file.build_params_hash(*args, **kwargs)
107
- @namespace_or_ops_file.invoke(@runtime_env, params_hash)
123
+ def _resolve_method_and_invoke(name, *args, **kwargs)
124
+ if _bang_method?(name) # foo! is an attempt to invoke the module's default entrypoint
125
+ method_name = _non_bang_method(name)
126
+ @namespace_or_ops_file = @namespace_or_ops_file.resolve_symbol(method_name)
127
+ _invoke_if_namespace_has_ops_file_of_same_name(*args, **kwargs)
128
+ else
129
+ @namespace_or_ops_file = @namespace_or_ops_file.resolve_symbol(name)
130
+ _invoke(*args, **kwargs)
131
+ end
108
132
  end
109
133
 
110
134
  # if this namespace contains an OpsFile of the same name as the namespace, e.g. pkg/install/install.ops, then this
111
135
  # method invokes the OpsFile of that same name and returns the result;
112
136
  # otherwise we return this namespace object
113
- def _invoke_if_namespace_has_ops_file_of_same_name(*args, **kwargs, &block)
137
+ def _invoke_if_namespace_has_ops_file_of_same_name(*args, **kwargs)
114
138
  method_name = @namespace_or_ops_file.dirname.basename
115
139
  resolved_symbol = @namespace_or_ops_file.resolve_symbol(method_name)
116
140
  if resolved_symbol.is_a? OpsFile
@@ -121,14 +145,20 @@ module OpsWalrus
121
145
  end
122
146
  end
123
147
 
124
- def _resolve_method_and_invoke(name, *args, **kwargs)
125
- @namespace_or_ops_file = @namespace_or_ops_file.resolve_symbol(name)
126
- _invoke(*args, **kwargs)
148
+ def _invoke(*args, **kwargs)
149
+ case @namespace_or_ops_file
150
+ when Namespace
151
+ self
152
+ when OpsFile
153
+ _invoke_local(*args, **kwargs)
154
+ end
127
155
  end
128
156
 
129
- def method_missing(name, *args, **kwargs, &block)
130
- _resolve_method_and_invoke(name, *args, **kwargs)
157
+ def _invoke_local(*args, **kwargs)
158
+ params_hash = @namespace_or_ops_file.build_params_hash(*args, **kwargs)
159
+ @namespace_or_ops_file.invoke(@runtime_env, params_hash)
131
160
  end
161
+
132
162
  end
133
163
 
134
164
  end
@@ -112,7 +112,6 @@ module SSHKit
112
112
  buffer || ""
113
113
  end
114
114
 
115
-
116
115
  def handle_data_for_stderr(output, cmd, buffer, stdin, is_blocked)
117
116
  # we're blocked on reading, so let's process the buffer
118
117
  lines, buffer = split_buffer(buffer)
@@ -28,24 +28,39 @@ module OpsWalrus
28
28
  @app.sudo_password
29
29
  end
30
30
 
31
- # runtime_kv_args is an Array(String) of the form: ["arg1:val1", "arg2:val2", ...]
32
- # params_json_hash is a Hash representation of a JSON string
33
- def run(runtime_kv_args, params_json_hash: nil)
34
- params_hash = runtime_kv_args.reduce(params_json_hash || {}) do |memo, kv_pair_string|
35
- str_key, str_value = kv_pair_string.split(":", 2)
36
- if pre_existing_value = memo[str_key]
37
- array = pre_existing_value.is_a?(Array) ? pre_existing_value : [pre_existing_value]
38
- array << str_value
39
- memo[str_key] = array
31
+ # runtime_kv_args is an Array(String) of the form: ["arg1:val1", "arg1:val2", ...]
32
+ # irb(main):057:0> build_params_hash(["names:foo", "names:bar", "names:baz", "age:5", "profile:name:corge", "profile:language:en", "height:5ft8in"])
33
+ # => {"names"=>["foo", "bar", "baz"], "age"=>"5", "profile"=>{"name"=>"corge", "language"=>"en"}, "height"=>"5ft8in"}
34
+ def build_params_hash(runtime_kv_args, params_json_hash: nil)
35
+ runtime_kv_args.reduce(params_json_hash || {}) do |memo, kv_pair_string|
36
+ param_name, str_value = kv_pair_string.split(":", 2)
37
+ key, value = str_value.split(":", 2)
38
+ if pre_existing_value = memo[param_name]
39
+ memo[param_name] = if value # we're dealing with a Hash parameter value
40
+ pre_existing_value.merge(key => value)
41
+ else # we're dealing with an Array parameter value or a scalar parameter value
42
+ array = pre_existing_value.is_a?(Array) ? pre_existing_value : [pre_existing_value]
43
+ array << str_value
44
+ end
40
45
  else
41
- memo[str_key] = str_value
46
+ memo[param_name] = if value # we're dealing with a Hash parameter value
47
+ {key => value}
48
+ else # we're dealing with an Array parameter value or a scalar parameter value
49
+ str_value
50
+ end
42
51
  end
43
52
  memo
44
53
  end
54
+ end
55
+
56
+ # runtime_kv_args is an Array(String) of the form: ["arg1:val1", "arg1:val2", ...]
57
+ # params_json_hash is a Hash representation of a JSON string
58
+ def run(runtime_kv_args, params_json_hash: nil)
59
+ params_hash = build_params_hash(runtime_kv_args, params_json_hash: params_json_hash)
45
60
 
46
61
  if app.debug?
47
- puts "Script:"
48
- puts @entry_point_ops_file.script
62
+ App.instance.trace "Script:"
63
+ App.instance.trace @entry_point_ops_file.script
49
64
  end
50
65
 
51
66
  result = begin
@@ -61,14 +76,14 @@ module OpsWalrus
61
76
  App.instance.error "[!] Command failed: #{e.message}"
62
77
  rescue Error => e
63
78
  App.instance.error "Error: Ops script crashed."
64
- App.instance.error e.message
65
- App.instance.error e.backtrace.take(5).join("\n")
79
+ App.instance.error e
80
+ # App.instance.error e.backtrace.take(5).join("\n")
66
81
  Invocation::Error.new(e)
67
82
  rescue => e
68
83
  App.instance.error "Unhandled Error: Ops script crashed."
69
84
  App.instance.error e.class
70
- App.instance.error e.message
71
- App.instance.error e.backtrace.take(10).join("\n")
85
+ App.instance.error e
86
+ # App.instance.error e.backtrace.take(10).join("\n")
72
87
  Invocation::Error.new(e)
73
88
  end
74
89
 
@@ -76,7 +91,7 @@ module OpsWalrus
76
91
  App.instance.debug "Ops script error details:"
77
92
  App.instance.debug "Error: #{result.value}"
78
93
  App.instance.debug "Status code: #{result.exit_status}"
79
- App.instance.debug @entry_point_ops_file.script
94
+ App.instance.debug @entry_point_ops_file.script.to_s
80
95
  end
81
96
 
82
97
  result
@@ -173,8 +173,9 @@ module OpsWalrus
173
173
  raise Error, "Unknown import reference: #{local_name}: #{import_str.inspect}"
174
174
  end
175
175
 
176
- def invoke(runtime_env, params_hash)
177
- script._invoke(runtime_env, params_hash)
176
+ def invoke(runtime_env, hashlike_params)
177
+ # this invokes the dynamically generated _invoke method that is defined at runtime within OpsFileScript.define_for(...)
178
+ script._invoke(runtime_env, hashlike_params)
178
179
  end
179
180
 
180
181
  def build_params_hash(*args, **kwargs)
@@ -1,9 +1,80 @@
1
+ require 'forwardable'
1
2
  require 'set'
2
3
  require_relative 'invocation'
3
4
  require_relative 'ops_file_script_dsl'
4
5
 
5
6
  module OpsWalrus
6
7
 
8
+ class ArrayOrHashNavigationProxy
9
+ extend Forwardable
10
+
11
+ def initialize(array_or_hash)
12
+ @obj = array_or_hash
13
+ end
14
+
15
+ def_delegators :@obj, :to_s, :inspect, :hash, :===, :eql?, :kind_of?, :is_a?, :instance_of?, :respond_to?, :<=>
16
+
17
+ def [](index, *args, **kwargs, &block)
18
+ @obj.method(:[]).call(index, *args, **kwargs, &block)
19
+ end
20
+ def respond_to_missing?(method, *)
21
+ @obj.is_a?(Hash) && @obj.respond_to?(method)
22
+ end
23
+ def method_missing(name, *args, **kwargs, &block)
24
+ case @obj
25
+ when Array
26
+ @obj.method(name).call(*args, **kwargs, &block)
27
+ when Hash
28
+ if @obj.respond_to?(name)
29
+ @obj.method(name).call(*args, **kwargs, &block)
30
+ else
31
+ value = self[name.to_s]
32
+ case value
33
+ when Array, Hash
34
+ ArrayOrHashNavigationProxy.new(value)
35
+ else
36
+ value
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+
43
+ class InvocationParams
44
+ # @params : Hash
45
+
46
+ # params : Hash | ArrayOrHashNavigationProxy
47
+ def initialize(hashlike_params)
48
+ # this doesn't seem to make any difference
49
+ @params = hashlike_params.to_h
50
+ # @params = hashlike_params
51
+ end
52
+
53
+ def [](key)
54
+ key = key.to_s if key.is_a? Symbol
55
+ @params[key]
56
+ end
57
+
58
+ def dig(*keys)
59
+ # keys = keys.map {|key| key.is_a?(Integer) ? key : key.to_s }
60
+ @params.dig(*keys)
61
+ end
62
+
63
+ def method_missing(name, *args, **kwargs, &block)
64
+ if @params.respond_to?(name)
65
+ @params.method(name).call(*args, **kwargs, &block)
66
+ else
67
+ value = self[name]
68
+ case value
69
+ when Array, Hash
70
+ ArrayOrHashNavigationProxy.new(value)
71
+ else
72
+ value
73
+ end
74
+ end
75
+ end
76
+ end
77
+
7
78
  class OpsFileScript
8
79
 
9
80
  def self.define_for(ops_file, ruby_script)
@@ -14,11 +85,11 @@ module OpsWalrus
14
85
  # define methods for the OpsFile's local_symbol_table: local imports and private lib directory
15
86
  ops_file.local_symbol_table.each do |symbol_name, import_reference|
16
87
  unless methods_defined.include? symbol_name
17
- App.instance.debug "defining method for local symbol table entry: #{symbol_name}"
88
+ App.instance.trace "defining method for local symbol table entry: #{symbol_name}"
18
89
  klass.define_method(symbol_name) do |*args, **kwargs, &block|
19
- App.instance.debug "resolving local symbol table entry: #{symbol_name}"
90
+ App.instance.trace "resolving local symbol table entry: #{symbol_name}"
20
91
  namespace_or_ops_file = @runtime_env.resolve_import_reference(ops_file, import_reference)
21
- App.instance.debug "namespace_or_ops_file=#{namespace_or_ops_file.to_s}"
92
+ App.instance.trace "namespace_or_ops_file=#{namespace_or_ops_file.to_s}"
22
93
 
23
94
  invocation_context = LocalImportInvocationContext.new(@runtime_env, namespace_or_ops_file)
24
95
  invocation_context._invoke(*args, **kwargs)
@@ -33,14 +104,14 @@ module OpsWalrus
33
104
  sibling_symbol_table_names |= ops_file.dirname.glob("*.ops").map {|ops_file_path| ops_file_path.basename(".ops").to_s } # OpsFiles
34
105
  sibling_symbol_table_names |= ops_file.dirname.glob("*").select(&:directory?).map {|dir_path| dir_path.basename.to_s } # Namespaces
35
106
  # puts "sibling_symbol_table_names=#{sibling_symbol_table_names}"
36
- App.instance.debug "methods_defined=#{methods_defined}"
107
+ App.instance.trace "methods_defined=#{methods_defined}"
37
108
  sibling_symbol_table_names.each do |symbol_name|
38
109
  unless methods_defined.include? symbol_name
39
- App.instance.debug "defining method for implicit imports: #{symbol_name}"
110
+ App.instance.trace "defining method for implicit imports: #{symbol_name}"
40
111
  klass.define_method(symbol_name) do |*args, **kwargs, &block|
41
- App.instance.debug "resolving implicit import: #{symbol_name}"
112
+ App.instance.trace "resolving implicit import: #{symbol_name}"
42
113
  namespace_or_ops_file = @runtime_env.resolve_sibling_symbol(ops_file, symbol_name)
43
- App.instance.debug "namespace_or_ops_file=#{namespace_or_ops_file.to_s}"
114
+ App.instance.trace "namespace_or_ops_file=#{namespace_or_ops_file.to_s}"
44
115
 
45
116
  invocation_context = LocalImportInvocationContext.new(@runtime_env, namespace_or_ops_file)
46
117
  invocation_context._invoke(*args, **kwargs)
@@ -60,9 +131,9 @@ module OpsWalrus
60
131
  # - #verbose?
61
132
  # - all the dynamically defined methods in the subclass of Invocation
62
133
  invoke_method_definition = <<~INVOKE_METHOD
63
- def _invoke(runtime_env, params_hash)
134
+ def _invoke(runtime_env, hashlike_params)
64
135
  @runtime_env = runtime_env
65
- @params = InvocationParams.new(params_hash)
136
+ @params = InvocationParams.new(hashlike_params)
66
137
  #{ruby_script}
67
138
  end
68
139
  INVOKE_METHOD
@@ -101,7 +172,7 @@ module OpsWalrus
101
172
  end
102
173
 
103
174
  # The _invoke method is dynamically defined as part of OpsFileScript.define_for
104
- def _invoke(runtime_env, params_hash)
175
+ def _invoke(runtime_env, hashlike_params)
105
176
  raise "Not implemented in base class."
106
177
  end
107
178
 
@@ -12,67 +12,6 @@ require_relative 'walrus_lang'
12
12
 
13
13
  module OpsWalrus
14
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
15
  module Invocation
77
16
  class Result
78
17
  attr_accessor :value
@@ -157,7 +96,7 @@ module OpsWalrus
157
96
 
158
97
  # puts retval.inspect
159
98
 
160
- # cleanup
99
+ # todo: cleanup
161
100
  # if tmp_bundle_root_dir =~ /tmp/ # sanity check the temp path before we blow away something we don't intend
162
101
  # host.execute(:rm, "-rf", "tmpopsbootstrap.sh", "tmpops.zip", tmp_bundle_root_dir)
163
102
  # else
@@ -275,19 +214,23 @@ module OpsWalrus
275
214
  # puts "shell! self: #{self.inspect}"
276
215
 
277
216
  if App.instance.report_mode?
278
- print "[#{@runtime_env.local_hostname}] "
217
+ puts Style.green("*" * 80)
218
+ print "[#{Style.blue(@runtime_env.local_hostname)}] "
279
219
  print "#{description}: " if description
280
- puts cmd
220
+ puts Style.yellow(cmd)
281
221
  end
282
222
 
283
223
  return unless cmd && !cmd.strip.empty?
284
224
 
285
- sshkit_cmd = @runtime_env.handle_input(input) do |interaction_handler|
286
- # self is a Module instance that is serving as the evaluation context in an instance of a subclass of an Invocation; see Invocation#evaluate
287
- backend.execute_cmd(cmd, interaction_handler: interaction_handler, verbosity: :info)
225
+ if App.instance.dry_run?
226
+ ["", "", 0]
227
+ else
228
+ sshkit_cmd = @runtime_env.handle_input(input) do |interaction_handler|
229
+ # self is a Module instance that is serving as the evaluation context in an instance of a subclass of an Invocation; see Invocation#evaluate
230
+ backend.execute_cmd(cmd, interaction_handler: interaction_handler, verbosity: :info)
231
+ end
232
+ [sshkit_cmd.full_stdout, sshkit_cmd.full_stderr, sshkit_cmd.exit_status]
288
233
  end
289
-
290
- [sshkit_cmd.full_stdout, sshkit_cmd.full_stderr, sshkit_cmd.exit_status]
291
234
  end
292
235
 
293
236
  # def init_brew
@@ -15,15 +15,6 @@ module OpsWalrus
15
15
  @local_name, @package_uri, @version = local_name, package_uri, version
16
16
  end
17
17
 
18
- def sanitized_package_uri
19
- sanitize_path(@package_uri)
20
- end
21
-
22
- def sanitize_path(path)
23
- # found this at https://apidock.com/rails/v5.2.3/ActiveStorage/Filename/sanitized
24
- path.encode(Encoding::UTF_8, invalid: :replace, undef: :replace, replace: "�").strip.tr("\u{202E}%$|:;/\t\r\n\\", "-")
25
- end
26
-
27
18
  # important: the import_resolution_dirname implemented as the local_name is critical because Bundler#download_package downloads
28
19
  # package dependencies to the name that this method returns, which must match the package reference's local name
29
20
  # so that later, when the package is being looked up on the load path (in LoadPath#resolve_import_reference),
@@ -34,7 +25,7 @@ module OpsWalrus
34
25
  # change in order for the three things to reconcile with respect to one another, since all three bits of logic are
35
26
  # what make bundling package dependencies and loading them function properly.
36
27
  def import_resolution_dirname
37
- local_name
28
+ "pkg_#{local_name}_version_#{version}"
38
29
  end
39
30
 
40
31
  def to_s
@@ -53,8 +44,17 @@ module OpsWalrus
53
44
  # these are dynamic package references defined at runtime when an OpsFile's imports are being evaluated.
54
45
  # this will usually be the case when an ops file does not belong to a package
55
46
  class DynamicPackageReference < PackageReference
47
+ def self.import_resolution_dirname(package_uri, version)
48
+ sanitized_package_uri = sanitize_path(package_uri || raise(Error, "Unspecified package reference"))
49
+ sanitized_version = sanitize_path(version || "")
50
+ "pkg_#{sanitized_package_uri}_version_#{sanitized_version}"
51
+ end
52
+ def self.sanitize_path(path)
53
+ # found this at https://apidock.com/rails/v5.2.3/ActiveStorage/Filename/sanitized
54
+ path.encode(Encoding::UTF_8, invalid: :replace, undef: :replace, replace: "�").strip.tr("\u{202E}%$|:;/\t\r\n\\", "-")
55
+ end
56
56
  def import_resolution_dirname
57
- sanitized_package_uri
57
+ DynamicPackageReference.import_resolution_dirname(@package_uri, @version)
58
58
  end
59
59
  end
60
60
 
@@ -90,7 +90,7 @@ module OpsWalrus
90
90
  str = "Namespace: #{@dirname.to_s}\n"
91
91
  @symbol_table.each do |k, v|
92
92
  if v.is_a? Namespace
93
- str << "#{' ' * (indent)}|- #{k} : #{v.to_s(indent + 1)}\n"
93
+ str << "#{' ' * (indent)}|- #{k} : #{v.to_s(indent + 1)}"
94
94
  else
95
95
  str << "#{' ' * (indent)}|- #{k} : #{v.to_s}\n"
96
96
  end
@@ -106,31 +106,6 @@ module OpsWalrus
106
106
  @symbol_table[symbol_name.to_s]
107
107
  end
108
108
 
109
- # # if this namespace contains an OpsFile of the same name as the namespace, e.g. pkg/install/install.ops, then this
110
- # # method invokes the OpsFile of that same name and returns the result;
111
- # # otherwise we return this namespace object
112
- # def _invoke_if_namespace_has_ops_file_of_same_name(*args, **kwargs, &block)
113
- # resolved_symbol = resolve_symbol(@dirname.basename)
114
- # if resolved_symbol.is_a? OpsFile
115
- # params_hash = resolved_symbol.build_params_hash(*args, **kwargs)
116
- # resolved_symbol.invoke(runtime_env, params_hash)
117
- # else
118
- # self
119
- # end
120
- # end
121
-
122
- # def method_missing(name, *args, **kwargs, &block)
123
- # # puts "method_missing: #{name}"
124
- # # puts caller
125
- # resolved_symbol = resolve_symbol(name)
126
- # case resolved_symbol
127
- # when Namespace
128
- # resolved_symbol._invoke_if_namespace_has_ops_file_of_same_name(*args, **kwargs)
129
- # when OpsFile
130
- # params_hash = resolved_symbol.build_params_hash(*args, **kwargs)
131
- # resolved_symbol.invoke(runtime_env, params_hash)
132
- # end
133
- # end
134
109
  end
135
110
 
136
111
  # the assumption is that we have a bundle directory with all the packages in it
@@ -147,16 +122,13 @@ module OpsWalrus
147
122
  @root_namespace = build_symbol_resolution_tree(@dir)
148
123
  @path_map = build_path_map(@root_namespace)
149
124
 
150
- # puts "*" * 80
151
- # puts "load path for #{@dir}"
152
- # puts "-" * 80
153
- # puts 'root namespace'
154
- # puts @root_namespace.to_s
155
- # puts "-" * 80
156
- # puts 'path map'
157
- # @path_map.each do |k,v|
158
- # puts "#{k.to_s}: #{v.to_s}"
159
- # end
125
+ App.instance.trace "LoadPath for #{@dir} ************************************************************"
126
+ App.instance.trace 'root namespace ******************************************************************'
127
+ App.instance.trace @root_namespace.to_s
128
+ App.instance.trace 'path map ************************************************************************'
129
+ @path_map.each do |k,v|
130
+ App.instance.trace "#{k.to_s}: #{v.to_s}"
131
+ end
160
132
 
161
133
  @dynamic_package_additions_memo = {}
162
134
  end
@@ -268,9 +240,16 @@ module OpsWalrus
268
240
  @bundle_load_path = LoadPath.new(self, @app.bundle_dir)
269
241
  @app_load_path = LoadPath.new(self, @app.pwd)
270
242
 
271
- @interaction_handler = ScopedMappingInteractionHandler.new({
272
- /\[sudo\] password for .*?:\s*/ => "#{sudo_password}\n",
273
- })
243
+ # we include this sudo password mapping by default because if a bundled script is being run on a remote host,
244
+ # then the remote command invocation will include the --pass flag when being run on the remote host, which will
245
+ # interactively prompt for a sudo password, and that will be the only opportunity for the command host
246
+ # that is running the bundled script on the remote host to interactively enter a sudo password for the remote context
247
+ # since the remote ops command will be running within a PTY, and the interactive prompts running on the remote
248
+ # host will be managed by the local ScopedMappingInteractionHandler running within the instance of the ops command
249
+ # process on the remote host, and the command host will not have any further opportunity to interactively enter
250
+ # any prompts on the remote host
251
+ interaction_handler_mapping_for_sudo_password = ScopedMappingInteractionHandler.mapping_for_sudo_password(sudo_password)
252
+ @interaction_handler = ScopedMappingInteractionHandler.new(interaction_handler_mapping_for_sudo_password)
274
253
 
275
254
  configure_sshkit
276
255
  end
@@ -291,6 +270,7 @@ module OpsWalrus
291
270
  # SSHKit.config.use_format :simpletext
292
271
  SSHKit.config.output_verbosity = :debug
293
272
  elsif app.verbose?
273
+ SSHKit.config.use_format :pretty
294
274
  # SSHKit.config.use_format :dot
295
275
  SSHKit.config.output_verbosity = :info
296
276
  end
@@ -344,8 +324,8 @@ module OpsWalrus
344
324
  end.run
345
325
  end
346
326
 
347
- def invoke(ops_file, params_hash)
348
- ops_file.invoke(self, params_hash)
327
+ def invoke(ops_file, hashlike_params)
328
+ ops_file.invoke(self, hashlike_params)
349
329
  end
350
330
 
351
331
  def find_load_path_that_includes_path(path)
@@ -1,3 +1,3 @@
1
1
  module OpsWalrus
2
- VERSION = "1.0.16"
2
+ VERSION = "1.0.17"
3
3
  end
data/lib/opswalrus.rb CHANGED
@@ -6,5 +6,5 @@ require_relative 'opswalrus/app'
6
6
  require_relative 'opswalrus/cli'
7
7
  require_relative 'opswalrus/version'
8
8
 
9
- module OpsWalrusWalrus
9
+ module OpsWalrus
10
10
  end
data/opswalrus.gemspec CHANGED
@@ -14,11 +14,11 @@ Gem::Specification.new do |spec|
14
14
  spec.license = "EPL-2.0"
15
15
  spec.required_ruby_version = ">= 2.6.0"
16
16
 
17
- # spec.metadata["allowed_push_host"] = "TODO: Set to your gem server - https://example.com"
17
+ # spec.metadata["allowed_push_host"] = "Set to your gem server - https://example.com"
18
18
 
19
19
  spec.metadata["homepage_uri"] = spec.homepage
20
20
  spec.metadata["source_code_uri"] = "https://github.com/opswalrus/opswalrus"
21
- # spec.metadata["changelog_uri"] = "TODO: Put your gem's CHANGELOG.md URL here."
21
+ # spec.metadata["changelog_uri"] = "Put your gem's CHANGELOG.md URL here."
22
22
 
23
23
  # Specify which files should be added to the gem when it is released.
24
24
  # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
@@ -37,7 +37,9 @@ Gem::Specification.new do |spec|
37
37
  spec.add_dependency "gli", "~> 2.21"
38
38
  spec.add_dependency "git", "~> 1.18"
39
39
  spec.add_dependency "ougai", "~> 2.0"
40
+ spec.add_dependency "pastel", "~> 0.8"
40
41
  spec.add_dependency "rubyzip", "~> 2.3"
42
+ spec.add_dependency "tty-editor", "~> 0.7"
41
43
 
42
44
  spec.add_dependency "bcrypt_pbkdf", "~> 1.1"
43
45
  spec.add_dependency "ed25519", "~> 1.3"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: opswalrus
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.16
4
+ version: 1.0.17
5
5
  platform: ruby
6
6
  authors:
7
7
  - David Ellis
@@ -80,6 +80,20 @@ dependencies:
80
80
  - - "~>"
81
81
  - !ruby/object:Gem::Version
82
82
  version: '2.0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: pastel
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '0.8'
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '0.8'
83
97
  - !ruby/object:Gem::Dependency
84
98
  name: rubyzip
85
99
  requirement: !ruby/object:Gem::Requirement
@@ -94,6 +108,20 @@ dependencies:
94
108
  - - "~>"
95
109
  - !ruby/object:Gem::Version
96
110
  version: '2.3'
111
+ - !ruby/object:Gem::Dependency
112
+ name: tty-editor
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: '0.7'
118
+ type: :runtime
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: '0.7'
97
125
  - !ruby/object:Gem::Dependency
98
126
  name: bcrypt_pbkdf
99
127
  requirement: !ruby/object:Gem::Requirement