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 +4 -4
- data/Gemfile.lock +18 -1
- data/lib/opswalrus/app.rb +51 -17
- data/lib/opswalrus/bootstrap.sh +1 -0
- data/lib/opswalrus/bundler.rb +18 -34
- data/lib/opswalrus/cli.rb +24 -10
- data/lib/opswalrus/host.rb +20 -10
- data/lib/opswalrus/interaction_handlers.rb +17 -16
- data/lib/opswalrus/invocation.rb +71 -41
- data/lib/opswalrus/local_non_blocking_backend.rb +0 -1
- data/lib/opswalrus/operation_runner.rb +32 -17
- data/lib/opswalrus/ops_file.rb +3 -2
- data/lib/opswalrus/ops_file_script.rb +81 -10
- data/lib/opswalrus/ops_file_script_dsl.rb +12 -69
- data/lib/opswalrus/package_file.rb +11 -11
- data/lib/opswalrus/runtime_environment.rb +21 -41
- data/lib/opswalrus/version.rb +1 -1
- data/lib/opswalrus.rb +1 -1
- data/opswalrus.gemspec +4 -2
- metadata +29 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f2ce759171a0342644a1e3e2c94555299b093095b1684a69e11142fe29292da4
|
4
|
+
data.tar.gz: 75f4cab88e00038c73df02d69718b5cd067f3ed69150c192335894128ef912d0
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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.
|
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
|
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
|
-
|
95
|
-
|
96
|
-
|
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
|
-
@
|
129
|
+
@logger.level <= 1
|
101
130
|
end
|
102
131
|
|
103
132
|
def debug?
|
104
|
-
@
|
133
|
+
@logger.level <= 0
|
134
|
+
end
|
135
|
+
|
136
|
+
def fatal(*args)
|
137
|
+
@logger.fatal(*args)
|
105
138
|
end
|
106
139
|
|
107
|
-
def error(
|
108
|
-
@logger.error(
|
140
|
+
def error(*args)
|
141
|
+
@logger.error(*args)
|
109
142
|
end
|
110
143
|
|
111
|
-
def warn(
|
112
|
-
@logger.warn(
|
144
|
+
def warn(*args)
|
145
|
+
@logger.warn(*args)
|
113
146
|
end
|
114
147
|
|
115
|
-
def log(
|
116
|
-
@logger.info(
|
148
|
+
def log(*args)
|
149
|
+
@logger.info(*args)
|
117
150
|
end
|
151
|
+
alias_method :info, :log
|
118
152
|
|
119
|
-
def debug(
|
120
|
-
@logger.debug(
|
153
|
+
def debug(*args)
|
154
|
+
@logger.debug(*args)
|
121
155
|
end
|
122
156
|
|
123
|
-
def trace(
|
124
|
-
@logger.trace(
|
157
|
+
def trace(*args)
|
158
|
+
@logger.trace(*args)
|
125
159
|
end
|
126
160
|
|
127
161
|
def set_pwd(pwd)
|
data/lib/opswalrus/bootstrap.sh
CHANGED
@@ -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
|
data/lib/opswalrus/bundler.rb
CHANGED
@@ -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
|
-
|
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 =
|
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["
|
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
|
-
|
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
|
|
data/lib/opswalrus/host.rb
CHANGED
@@ -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
|
-
|
126
|
-
|
127
|
-
|
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
|
-
|
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
|
-
|
66
|
+
trace(Style.red("No interaction handler mapping for #{stream_name}: #{data} so no response was sent"))
|
69
67
|
else
|
70
|
-
|
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
|
84
|
-
|
85
|
-
|
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
|
-
|
133
|
+
nil
|
132
134
|
else
|
133
135
|
raise "Unexpected prompt: #{data} on stream #{stream_name} and channel #{channel.inspect}"
|
134
|
-
|
136
|
+
end
|
135
137
|
end
|
136
138
|
end
|
137
139
|
|
138
|
-
|
139
140
|
end
|
data/lib/opswalrus/invocation.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
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
|
-
|
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
|
97
|
-
|
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
|
106
|
-
|
107
|
-
|
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
|
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
|
125
|
-
|
126
|
-
|
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
|
130
|
-
|
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
|
@@ -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", "
|
32
|
-
#
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
memo[
|
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[
|
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
|
-
|
48
|
-
|
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
|
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
|
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
|
data/lib/opswalrus/ops_file.rb
CHANGED
@@ -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,
|
177
|
-
|
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.
|
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.
|
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.
|
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.
|
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.
|
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.
|
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.
|
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,
|
134
|
+
def _invoke(runtime_env, hashlike_params)
|
64
135
|
@runtime_env = runtime_env
|
65
|
-
@params = InvocationParams.new(
|
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,
|
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
|
-
|
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
|
-
|
286
|
-
|
287
|
-
|
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
|
-
|
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)}
|
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
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
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
|
-
|
272
|
-
|
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,
|
348
|
-
ops_file.invoke(self,
|
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)
|
data/lib/opswalrus/version.rb
CHANGED
data/lib/opswalrus.rb
CHANGED
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"] = "
|
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"] = "
|
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.
|
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
|