bolt 2.16.0 → 2.21.0
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of bolt might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/Puppetfile +3 -1
- data/bolt-modules/boltlib/lib/puppet/functions/apply_prep.rb +20 -9
- data/bolt-modules/boltlib/lib/puppet/functions/download_file.rb +123 -0
- data/bolt-modules/boltlib/lib/puppet/functions/run_command.rb +2 -0
- data/bolt-modules/boltlib/lib/puppet/functions/run_plan.rb +6 -0
- data/bolt-modules/boltlib/lib/puppet/functions/run_script.rb +6 -4
- data/bolt-modules/dir/lib/puppet/functions/dir/children.rb +35 -0
- data/lib/bolt/applicator.rb +19 -14
- data/lib/bolt/apply_result.rb +1 -1
- data/lib/bolt/bolt_option_parser.rb +60 -16
- data/lib/bolt/catalog.rb +3 -2
- data/lib/bolt/cli.rb +121 -43
- data/lib/bolt/config.rb +37 -34
- data/lib/bolt/config/options.rb +340 -173
- data/lib/bolt/config/transport/options.rb +315 -160
- data/lib/bolt/config/transport/ssh.rb +24 -10
- data/lib/bolt/executor.rb +21 -0
- data/lib/bolt/inventory/group.rb +3 -2
- data/lib/bolt/inventory/inventory.rb +4 -3
- data/lib/bolt/logger.rb +24 -1
- data/lib/bolt/outputter.rb +1 -1
- data/lib/bolt/outputter/rainbow.rb +14 -3
- data/lib/bolt/pal.rb +28 -10
- data/lib/bolt/pal/yaml_plan/evaluator.rb +23 -2
- data/lib/bolt/pal/yaml_plan/step.rb +24 -2
- data/lib/bolt/pal/yaml_plan/step/download.rb +38 -0
- data/lib/bolt/pal/yaml_plan/step/message.rb +30 -0
- data/lib/bolt/pal/yaml_plan/step/upload.rb +3 -3
- data/lib/bolt/plugin/module.rb +2 -4
- data/lib/bolt/plugin/puppetdb.rb +3 -2
- data/lib/bolt/project.rb +20 -6
- data/lib/bolt/puppetdb/client.rb +2 -0
- data/lib/bolt/puppetdb/config.rb +16 -0
- data/lib/bolt/result.rb +7 -0
- data/lib/bolt/shell/bash.rb +45 -37
- data/lib/bolt/shell/powershell.rb +21 -11
- data/lib/bolt/shell/powershell/snippets.rb +15 -6
- data/lib/bolt/transport/base.rb +24 -0
- data/lib/bolt/transport/docker.rb +16 -4
- data/lib/bolt/transport/docker/connection.rb +20 -2
- data/lib/bolt/transport/local/connection.rb +14 -1
- data/lib/bolt/transport/orch.rb +20 -0
- data/lib/bolt/transport/simple.rb +6 -0
- data/lib/bolt/transport/ssh.rb +7 -1
- data/lib/bolt/transport/ssh/connection.rb +9 -1
- data/lib/bolt/transport/ssh/exec_connection.rb +23 -2
- data/lib/bolt/transport/winrm/connection.rb +118 -8
- data/lib/bolt/util.rb +26 -11
- data/lib/bolt/version.rb +1 -1
- data/lib/bolt_server/transport_app.rb +3 -2
- data/lib/bolt_spec/bolt_context.rb +7 -2
- data/lib/bolt_spec/plans.rb +15 -2
- data/lib/bolt_spec/plans/action_stubs.rb +2 -1
- data/lib/bolt_spec/plans/action_stubs/download_stub.rb +66 -0
- data/lib/bolt_spec/plans/mock_executor.rb +14 -1
- data/lib/bolt_spec/run.rb +22 -0
- data/libexec/bolt_catalog +3 -2
- data/modules/secure_env_vars/plans/init.pp +20 -0
- metadata +21 -29
@@ -85,12 +85,21 @@ module Bolt
|
|
85
85
|
|
86
86
|
def shell_init
|
87
87
|
<<~PS
|
88
|
-
$
|
89
|
-
|
90
|
-
|
91
|
-
$
|
92
|
-
|
93
|
-
|
88
|
+
$installRegKey = Get-ItemProperty -Path "HKLM:\\Software\\Puppet Labs\\Puppet" -ErrorAction 0
|
89
|
+
if(![string]::IsNullOrEmpty($installRegKey.RememberedInstallDir64)){
|
90
|
+
$boltBaseDir = $installRegKey.RememberedInstallDir64
|
91
|
+
}elseif(![string]::IsNullOrEmpty($installRegKey.RememberedInstallDir)){
|
92
|
+
$boltBaseDir = $installRegKey.RememberedInstallDir
|
93
|
+
}else{
|
94
|
+
$boltBaseDir = "${ENV:ProgramFiles}\\Puppet Labs\\Puppet"
|
95
|
+
}
|
96
|
+
|
97
|
+
$ENV:PATH += ";${boltBaseDir}\\bin\\;" +
|
98
|
+
"${boltBaseDir}\\puppet\\bin;" +
|
99
|
+
"${boltBaseDir}\\sys\\ruby\\bin\\"
|
100
|
+
$ENV:RUBYLIB = "${boltBaseDir}\\puppet\\lib;" +
|
101
|
+
"${boltBaseDir}\\facter\\lib;" +
|
102
|
+
"${boltBaseDir}\\hiera\\lib;" +
|
94
103
|
$ENV:RUBYLIB
|
95
104
|
|
96
105
|
Add-Type -AssemblyName System.ServiceModel.Web, System.Runtime.Serialization
|
data/lib/bolt/transport/base.rb
CHANGED
@@ -167,6 +167,25 @@ module Bolt
|
|
167
167
|
end
|
168
168
|
end
|
169
169
|
|
170
|
+
# Downloads the given source file from a batch of targets to the destination location
|
171
|
+
# on the host.
|
172
|
+
#
|
173
|
+
# The default implementation only supports batches of size 1 and will fail otherwise.
|
174
|
+
#
|
175
|
+
# Transports may override this method to implement their own batch processing.
|
176
|
+
def batch_download(targets, source, destination, options = {}, &callback)
|
177
|
+
require 'erb'
|
178
|
+
|
179
|
+
assert_batch_size_one("batch_download()", targets)
|
180
|
+
target = targets.first
|
181
|
+
with_events(target, callback, 'download') do
|
182
|
+
escaped_name = ERB::Util.url_encode(target.safe_name)
|
183
|
+
target_destination = File.expand_path(escaped_name, destination)
|
184
|
+
@logger.debug { "Downloading: '#{source}' on #{target.safe_name} to #{target_destination}" }
|
185
|
+
download(target, source, target_destination, options)
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
170
189
|
def batch_connected?(targets)
|
171
190
|
assert_batch_size_one("connected?()", targets)
|
172
191
|
connected?(targets.first)
|
@@ -201,6 +220,11 @@ module Bolt
|
|
201
220
|
raise NotImplementedError, "upload() must be implemented by the transport class"
|
202
221
|
end
|
203
222
|
|
223
|
+
# Transports should override this method with their own implementation of file download.
|
224
|
+
def download(*_args)
|
225
|
+
raise NotImplementedError, "download() must be implemented by the transport class"
|
226
|
+
end
|
227
|
+
|
204
228
|
# Transports should override this method with their own implementation of a connection test.
|
205
229
|
def connected?(_targets)
|
206
230
|
raise NotImplementedError, "connected?() must be implemented by the transport class"
|
@@ -38,8 +38,18 @@ module Bolt
|
|
38
38
|
end
|
39
39
|
end
|
40
40
|
|
41
|
+
def download(target, source, destination, _options = {})
|
42
|
+
with_connection(target) do |conn|
|
43
|
+
download = File.join(destination, Bolt::Util.unix_basename(source))
|
44
|
+
conn.download_remote_content(source, destination)
|
45
|
+
Bolt::Result.for_download(target, source, destination, download)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
41
49
|
def run_command(target, command, options = {})
|
42
|
-
|
50
|
+
execute_options = {}
|
51
|
+
execute_options[:tty] = target.options['tty']
|
52
|
+
execute_options[:environment] = options[:env_vars]
|
43
53
|
|
44
54
|
if target.options['shell-command'] && !target.options['shell-command'].empty?
|
45
55
|
# escape any double quotes in command
|
@@ -47,19 +57,21 @@ module Bolt
|
|
47
57
|
command = "#{target.options['shell-command']} \" #{command}\""
|
48
58
|
end
|
49
59
|
with_connection(target) do |conn|
|
50
|
-
stdout, stderr, exitcode = conn.execute(*Shellwords.split(command),
|
60
|
+
stdout, stderr, exitcode = conn.execute(*Shellwords.split(command), execute_options)
|
51
61
|
Bolt::Result.for_command(target, stdout, stderr, exitcode, 'command', command)
|
52
62
|
end
|
53
63
|
end
|
54
64
|
|
55
|
-
def run_script(target, script, arguments,
|
65
|
+
def run_script(target, script, arguments, options = {})
|
56
66
|
# unpack any Sensitive data
|
57
67
|
arguments = unwrap_sensitive_args(arguments)
|
68
|
+
execute_options = {}
|
69
|
+
execute_options[:environment] = options[:env_vars]
|
58
70
|
|
59
71
|
with_connection(target) do |conn|
|
60
72
|
conn.with_remote_tmpdir do |dir|
|
61
73
|
remote_path = conn.write_remote_executable(dir, script)
|
62
|
-
stdout, stderr, exitcode = conn.execute(remote_path, *arguments,
|
74
|
+
stdout, stderr, exitcode = conn.execute(remote_path, *arguments, execute_options)
|
63
75
|
Bolt::Result.for_command(target, stdout, stderr, exitcode, 'script', script)
|
64
76
|
end
|
65
77
|
end
|
@@ -82,7 +82,9 @@ module Bolt
|
|
82
82
|
def write_remote_file(source, destination)
|
83
83
|
@logger.debug { "Uploading #{source}, to #{destination}" }
|
84
84
|
_, stdout_str, status = execute_local_docker_command('cp', [source, "#{container_id}:#{destination}"])
|
85
|
-
|
85
|
+
unless status.exitstatus.zero?
|
86
|
+
raise "Error writing file to container #{@container_id}: #{stdout_str}"
|
87
|
+
end
|
86
88
|
rescue StandardError => e
|
87
89
|
raise Bolt::Node::FileError.new(e.message, 'WRITE_ERROR')
|
88
90
|
end
|
@@ -90,7 +92,23 @@ module Bolt
|
|
90
92
|
def write_remote_directory(source, destination)
|
91
93
|
@logger.debug { "Uploading #{source}, to #{destination}" }
|
92
94
|
_, stdout_str, status = execute_local_docker_command('cp', [source, "#{container_id}:#{destination}"])
|
93
|
-
|
95
|
+
unless status.exitstatus.zero?
|
96
|
+
raise "Error writing directory to container #{@container_id}: #{stdout_str}"
|
97
|
+
end
|
98
|
+
rescue StandardError => e
|
99
|
+
raise Bolt::Node::FileError.new(e.message, 'WRITE_ERROR')
|
100
|
+
end
|
101
|
+
|
102
|
+
def download_remote_content(source, destination)
|
103
|
+
@logger.debug { "Downloading #{source} to #{destination}" }
|
104
|
+
# Create the destination directory, otherwise copying a source directory with Docker will
|
105
|
+
# copy the *contents* of the directory.
|
106
|
+
# https://docs.docker.com/engine/reference/commandline/cp/
|
107
|
+
FileUtils.mkdir_p(destination)
|
108
|
+
_, stdout_str, status = execute_local_docker_command('cp', ["#{container_id}:#{source}", destination])
|
109
|
+
unless status.exitstatus.zero?
|
110
|
+
raise "Error downloading content from container #{@container_id}: #{stdout_str}"
|
111
|
+
end
|
94
112
|
rescue StandardError => e
|
95
113
|
raise Bolt::Node::FileError.new(e.message, 'WRITE_ERROR')
|
96
114
|
end
|
@@ -27,7 +27,7 @@ module Bolt
|
|
27
27
|
end
|
28
28
|
end
|
29
29
|
|
30
|
-
def
|
30
|
+
def upload_file(source, dest)
|
31
31
|
@logger.debug { "Uploading #{source}, to #{dest}" }
|
32
32
|
if source.is_a?(StringIO)
|
33
33
|
Tempfile.create(File.basename(dest)) do |f|
|
@@ -45,6 +45,19 @@ module Bolt
|
|
45
45
|
raise Bolt::Node::FileError.new(message, 'COPY_ERROR')
|
46
46
|
end
|
47
47
|
|
48
|
+
def download_file(source, dest, _download)
|
49
|
+
@logger.debug { "Downloading #{source} to #{dest}" }
|
50
|
+
# Create the destination directory for the target, or the
|
51
|
+
# copied file will have the target's name
|
52
|
+
FileUtils.mkdir_p(dest)
|
53
|
+
# Mimic the behavior of `cp --remove-destination`
|
54
|
+
# since the flag isn't supported on MacOS
|
55
|
+
FileUtils.cp_r(source, dest, remove_destination: true)
|
56
|
+
rescue StandardError => e
|
57
|
+
message = "Could not download file to #{dest}: #{e}"
|
58
|
+
raise Bolt::Node::FileError.new(message, 'DOWNLOAD_ERROR')
|
59
|
+
end
|
60
|
+
|
48
61
|
def execute(command)
|
49
62
|
if Bolt::Util.windows?
|
50
63
|
# If it's already a powershell command then invoke it normally.
|
data/lib/bolt/transport/orch.rb
CHANGED
@@ -82,6 +82,10 @@ module Bolt
|
|
82
82
|
end
|
83
83
|
|
84
84
|
def batch_command(targets, command, options = {}, &callback)
|
85
|
+
if options[:env_vars] && !options[:env_vars].empty?
|
86
|
+
raise NotImplementedError, "pcp transport does not support setting environment variables"
|
87
|
+
end
|
88
|
+
|
85
89
|
params = {
|
86
90
|
'command' => command
|
87
91
|
}
|
@@ -98,6 +102,10 @@ module Bolt
|
|
98
102
|
end
|
99
103
|
|
100
104
|
def batch_script(targets, script, arguments, options = {}, &callback)
|
105
|
+
if options[:env_vars] && !options[:env_vars].empty?
|
106
|
+
raise NotImplementedError, "pcp transport does not support setting environment variables"
|
107
|
+
end
|
108
|
+
|
101
109
|
content = File.open(script, &:read)
|
102
110
|
content = Base64.encode64(content)
|
103
111
|
params = {
|
@@ -176,6 +184,18 @@ module Bolt
|
|
176
184
|
end
|
177
185
|
end
|
178
186
|
|
187
|
+
def batch_download(targets, *_args)
|
188
|
+
error = {
|
189
|
+
'kind' => 'bolt/not-supported-error',
|
190
|
+
'msg' => 'pcp transport does not support downloading files',
|
191
|
+
'details' => {}
|
192
|
+
}
|
193
|
+
|
194
|
+
targets.map do |target|
|
195
|
+
Bolt::Result.new(target, error: error, action: 'download')
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
179
199
|
def batches(targets)
|
180
200
|
targets.group_by { |target| Connection.get_key(target.options) }.values
|
181
201
|
end
|
@@ -32,6 +32,12 @@ module Bolt
|
|
32
32
|
end
|
33
33
|
end
|
34
34
|
|
35
|
+
def download(target, source, destination, options = {})
|
36
|
+
with_connection(target) do |conn|
|
37
|
+
conn.shell.download(source, destination, options)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
35
41
|
def run_script(target, script, arguments, options = {})
|
36
42
|
with_connection(target) do |conn|
|
37
43
|
conn.shell.run_script(script, arguments, options)
|
data/lib/bolt/transport/ssh.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'bolt/logger'
|
3
4
|
require 'bolt/node/errors'
|
4
5
|
require 'bolt/transport/simple'
|
5
6
|
|
@@ -21,7 +22,12 @@ module Bolt
|
|
21
22
|
end
|
22
23
|
|
23
24
|
def with_connection(target)
|
24
|
-
|
25
|
+
if target.transport_config['ssh-command'] && !target.transport_config['native-ssh']
|
26
|
+
Bolt::Logger.warn_once("ssh-command and native-ssh conflict",
|
27
|
+
"native-ssh must be true to use ssh-command")
|
28
|
+
end
|
29
|
+
|
30
|
+
conn = if target.transport_config['native-ssh']
|
25
31
|
ExecConnection.new(target)
|
26
32
|
else
|
27
33
|
Connection.new(target, @transport_logger)
|
@@ -235,7 +235,7 @@ module Bolt
|
|
235
235
|
raise Bolt::Error.new(msg, 'bolt/too-many-files')
|
236
236
|
end
|
237
237
|
|
238
|
-
def
|
238
|
+
def upload_file(source, destination)
|
239
239
|
# Do not log wrapper script content
|
240
240
|
@logger.debug { "Uploading #{source}, to #{destination}" } unless source.is_a?(StringIO)
|
241
241
|
@session.scp.upload!(source, destination, recursive: true)
|
@@ -243,6 +243,14 @@ module Bolt
|
|
243
243
|
raise Bolt::Node::FileError.new(e.message, 'WRITE_ERROR')
|
244
244
|
end
|
245
245
|
|
246
|
+
def download_file(source, destination, _download)
|
247
|
+
# Do not log wrapper script content
|
248
|
+
@logger.debug { "Downloading #{source} to #{destination}" }
|
249
|
+
@session.scp.download!(source, destination, recursive: true)
|
250
|
+
rescue StandardError => e
|
251
|
+
raise Bolt::Node::FileError.new(e.message, 'WRITE_ERROR')
|
252
|
+
end
|
253
|
+
|
246
254
|
# This handles renaming Net::SSH verifiers between version 4.x and 5.x
|
247
255
|
# of the gem
|
248
256
|
def net_ssh_verifier(verifier)
|
@@ -58,14 +58,14 @@ module Bolt
|
|
58
58
|
end
|
59
59
|
|
60
60
|
def build_ssh_command(command)
|
61
|
-
ssh_conf = @target.transport_config['ssh-command']
|
61
|
+
ssh_conf = @target.transport_config['ssh-command'] || 'ssh'
|
62
62
|
ssh_cmd = Array(ssh_conf)
|
63
63
|
ssh_cmd += ssh_opts
|
64
64
|
ssh_cmd << userhost
|
65
65
|
ssh_cmd << command
|
66
66
|
end
|
67
67
|
|
68
|
-
def
|
68
|
+
def upload_file(source, dest)
|
69
69
|
@logger.debug { "Uploading #{source}, to #{userhost}:#{dest}" } unless source.is_a?(StringIO)
|
70
70
|
|
71
71
|
cp_conf = @target.transport_config['copy-command'] || ["scp", "-r"]
|
@@ -94,6 +94,27 @@ module Bolt
|
|
94
94
|
end
|
95
95
|
end
|
96
96
|
|
97
|
+
def download_file(source, dest, _download)
|
98
|
+
@logger.debug { "Downloading #{userhost}:#{source} to #{dest}" }
|
99
|
+
|
100
|
+
FileUtils.mkdir_p(dest)
|
101
|
+
|
102
|
+
cp_conf = @target.transport_config['copy-command'] || ["scp", "-r"]
|
103
|
+
cp_cmd = Array(cp_conf)
|
104
|
+
cp_cmd += ssh_opts
|
105
|
+
cp_cmd << "#{userhost}:#{Shellwords.escape(source)}"
|
106
|
+
cp_cmd << dest
|
107
|
+
|
108
|
+
_, err, stat = Open3.capture3(*cp_cmd)
|
109
|
+
|
110
|
+
if stat.success?
|
111
|
+
@logger.debug "Successfully downloaded #{userhost}:#{source} to #{dest}"
|
112
|
+
else
|
113
|
+
message = "Could not copy file to #{dest}: #{err}"
|
114
|
+
raise Bolt::Node::FileError.new(message, 'COPY_ERROR')
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
97
118
|
def execute(command)
|
98
119
|
cmd_array = build_ssh_command(command)
|
99
120
|
Open3.popen3(*cmd_array)
|
@@ -111,12 +111,21 @@ module Bolt
|
|
111
111
|
out_rd, out_wr = IO.pipe('UTF-8')
|
112
112
|
err_rd, err_wr = IO.pipe('UTF-8')
|
113
113
|
th = Thread.new do
|
114
|
+
# By default, any exception raised in a thread will be reported to
|
115
|
+
# stderr as a stacktrace. Since we know these errors are going to
|
116
|
+
# propagate to the main thread via the shell, there's no chance
|
117
|
+
# they will be unhandled, so the default stack trace is unneeded.
|
118
|
+
Thread.current.report_on_exception = false
|
114
119
|
result = @session.run(command)
|
115
120
|
out_wr << result.stdout
|
116
121
|
err_wr << result.stderr
|
117
122
|
out_wr.close
|
118
123
|
err_wr.close
|
119
124
|
result.exitcode
|
125
|
+
ensure
|
126
|
+
# Close the streams to avoid the caller deadlocking
|
127
|
+
out_wr.close
|
128
|
+
err_wr.close
|
120
129
|
end
|
121
130
|
|
122
131
|
[inp, out_rd, err_rd, th]
|
@@ -129,23 +138,23 @@ module Bolt
|
|
129
138
|
raise
|
130
139
|
end
|
131
140
|
|
132
|
-
def
|
141
|
+
def upload_file(source, destination)
|
133
142
|
@logger.debug { "Uploading #{source}, to #{destination}" }
|
134
143
|
if target.options['file-protocol'] == 'smb'
|
135
|
-
|
144
|
+
upload_file_smb(source, destination)
|
136
145
|
else
|
137
|
-
|
146
|
+
upload_file_winrm(source, destination)
|
138
147
|
end
|
139
148
|
end
|
140
149
|
|
141
|
-
def
|
150
|
+
def upload_file_winrm(source, destination)
|
142
151
|
fs = ::WinRM::FS::FileManager.new(@connection)
|
143
152
|
fs.upload(source, destination)
|
144
153
|
rescue StandardError => e
|
145
154
|
raise Bolt::Node::FileError.new(e.message, 'WRITE_ERROR')
|
146
155
|
end
|
147
156
|
|
148
|
-
def
|
157
|
+
def upload_file_smb(source, destination)
|
149
158
|
# lazy-load expensive gem code
|
150
159
|
require 'ruby_smb'
|
151
160
|
|
@@ -165,7 +174,7 @@ module Bolt
|
|
165
174
|
client = smb_client_login
|
166
175
|
tree = client.tree_connect(path)
|
167
176
|
begin
|
168
|
-
|
177
|
+
upload_file_smb_recursive(tree, source, dest)
|
169
178
|
ensure
|
170
179
|
tree.disconnect!
|
171
180
|
end
|
@@ -175,6 +184,61 @@ module Bolt
|
|
175
184
|
raise Bolt::Node::FileError.new(e.message, 'WRITE_ERROR')
|
176
185
|
end
|
177
186
|
|
187
|
+
def download_file(source, destination, download)
|
188
|
+
@logger.debug { "Downloading #{source} to #{destination}" }
|
189
|
+
if target.options['file-protocol'] == 'smb'
|
190
|
+
download_file_smb(source, destination)
|
191
|
+
else
|
192
|
+
download_file_winrm(source, destination, download)
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
def download_file_winrm(source, destination, download)
|
197
|
+
# The winrm gem doesn't create the destination directory if it's missing,
|
198
|
+
# so create it here
|
199
|
+
FileUtils.mkdir_p(destination)
|
200
|
+
fs = ::WinRM::FS::FileManager.new(@connection)
|
201
|
+
# params: source, destination, chunksize, first
|
202
|
+
# first needs to be set to false, otherwise if the source is a directory it
|
203
|
+
# will be nested inside a directory with the same name
|
204
|
+
fs.download(source, download, 1024 * 1024, false)
|
205
|
+
rescue StandardError => e
|
206
|
+
raise Bolt::Node::FileError.new(e.message, 'WRITE_ERROR')
|
207
|
+
end
|
208
|
+
|
209
|
+
def download_file_smb(source, destination)
|
210
|
+
# lazy-load expensive gem code
|
211
|
+
require 'ruby_smb'
|
212
|
+
|
213
|
+
win_source = source.tr('/', '\\')
|
214
|
+
if (md = win_source.match(/^([a-z]):\\(.*)/i))
|
215
|
+
# if drive, use admin share for that drive, so path is '\\host\C$'
|
216
|
+
path = "\\\\#{@target.host}\\#{md[1]}$"
|
217
|
+
src = md[2]
|
218
|
+
elsif (md = win_source.match(/^(\\\\[^\\]+\\[^\\]+)\\(.*)/))
|
219
|
+
# if unc, path is '\\host\share'
|
220
|
+
path = md[1]
|
221
|
+
src = md[2]
|
222
|
+
else
|
223
|
+
raise ArgumentError, "Unknown source '#{source}'"
|
224
|
+
end
|
225
|
+
|
226
|
+
client = smb_client_login
|
227
|
+
tree = client.tree_connect(path)
|
228
|
+
|
229
|
+
begin
|
230
|
+
# Make sure the root download directory for the target exists
|
231
|
+
FileUtils.mkdir_p(destination)
|
232
|
+
download_file_smb_recursive(tree, src, destination)
|
233
|
+
ensure
|
234
|
+
tree.disconnect!
|
235
|
+
end
|
236
|
+
rescue ::RubySMB::Error::UnexpectedStatusCode => e
|
237
|
+
raise Bolt::Node::FileError.new("SMB Error: #{e.message}", 'DOWNLOAD_ERROR')
|
238
|
+
rescue StandardError => e
|
239
|
+
raise Bolt::Node::FileError.new(e.message, 'DOWNLOAD_ERROR')
|
240
|
+
end
|
241
|
+
|
178
242
|
def shell
|
179
243
|
@shell ||= Bolt::Shell::Powershell.new(target, self)
|
180
244
|
end
|
@@ -230,13 +294,13 @@ module Bolt
|
|
230
294
|
)
|
231
295
|
end
|
232
296
|
|
233
|
-
def
|
297
|
+
def upload_file_smb_recursive(tree, source, dest)
|
234
298
|
if Dir.exist?(source)
|
235
299
|
tree.open_directory(directory: dest, write: true, disposition: ::RubySMB::Dispositions::FILE_OPEN_IF)
|
236
300
|
|
237
301
|
Dir.children(source).each do |child|
|
238
302
|
child_dest = dest + '\\' + child
|
239
|
-
|
303
|
+
upload_file_smb_recursive(tree, File.join(source, child), child_dest)
|
240
304
|
end
|
241
305
|
return
|
242
306
|
end
|
@@ -255,6 +319,52 @@ module Bolt
|
|
255
319
|
file.close
|
256
320
|
end
|
257
321
|
end
|
322
|
+
|
323
|
+
def download_file_smb_recursive(tree, source, destination)
|
324
|
+
dest = File.expand_path(Bolt::Util.windows_basename(source), destination)
|
325
|
+
|
326
|
+
# Check if the source is a directory by attempting to list its children.
|
327
|
+
# If the source is a directory, create the directory on the host and then
|
328
|
+
# recurse through the children.
|
329
|
+
if (children = list_directory_children_smb(tree, source))
|
330
|
+
FileUtils.mkdir_p(dest)
|
331
|
+
|
332
|
+
children.each do |child|
|
333
|
+
# File names are encoded UTF_16LE.
|
334
|
+
filename = child.file_name.encode(Encoding::UTF_8)
|
335
|
+
|
336
|
+
next if %w[. ..].include?(filename)
|
337
|
+
|
338
|
+
src = source + '\\' + filename
|
339
|
+
download_file_smb_recursive(tree, src, dest)
|
340
|
+
end
|
341
|
+
# If the source wasn't a directory and just returns 'STATUS_NOT_A_DIRECTORY, then
|
342
|
+
# it is a file. Write it to the host.
|
343
|
+
else
|
344
|
+
begin
|
345
|
+
file = tree.open_file(filename: source)
|
346
|
+
data = file.read
|
347
|
+
|
348
|
+
# Files may be encoded UTF_16LE
|
349
|
+
data = data.encode(Encoding::UTF_8) if data.encoding == Encoding::UTF_16LE
|
350
|
+
|
351
|
+
File.write(dest, data)
|
352
|
+
ensure
|
353
|
+
file.close
|
354
|
+
end
|
355
|
+
end
|
356
|
+
end
|
357
|
+
|
358
|
+
# Lists the children of a directory using rb_smb
|
359
|
+
# Returns an array of RubySMB::Fscc::FileInformation::FileIdFullDirectoryInformation objects
|
360
|
+
# if the source is a directory, or raises RubySMB::Error::UnexpectedStatusCode otherwise.
|
361
|
+
def list_directory_children_smb(tree, source)
|
362
|
+
tree.list(directory: source)
|
363
|
+
rescue RubySMB::Error::UnexpectedStatusCode => e
|
364
|
+
unless e.message == 'STATUS_NOT_A_DIRECTORY'
|
365
|
+
raise e
|
366
|
+
end
|
367
|
+
end
|
258
368
|
end
|
259
369
|
end
|
260
370
|
end
|