bolt 2.19.0 → 2.20.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/bolt-modules/boltlib/lib/puppet/functions/download_file.rb +123 -0
- data/lib/bolt/bolt_option_parser.rb +25 -2
- data/lib/bolt/catalog.rb +3 -2
- data/lib/bolt/cli.rb +139 -92
- data/lib/bolt/config.rb +1 -1
- data/lib/bolt/config/options.rb +14 -0
- data/lib/bolt/executor.rb +15 -0
- data/lib/bolt/inventory/group.rb +3 -2
- data/lib/bolt/inventory/inventory.rb +4 -3
- data/lib/bolt/outputter/rainbow.rb +3 -2
- data/lib/bolt/pal.rb +8 -2
- data/lib/bolt/pal/yaml_plan/evaluator.rb +18 -1
- data/lib/bolt/pal/yaml_plan/step.rb +11 -2
- data/lib/bolt/pal/yaml_plan/step/download.rb +38 -0
- data/lib/bolt/pal/yaml_plan/step/upload.rb +3 -3
- data/lib/bolt/plugin/puppetdb.rb +3 -2
- data/lib/bolt/project.rb +2 -1
- 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 +24 -4
- data/lib/bolt/shell/powershell.rb +10 -4
- data/lib/bolt/transport/base.rb +24 -0
- data/lib/bolt/transport/docker.rb +8 -0
- data/lib/bolt/transport/docker/connection.rb +20 -2
- data/lib/bolt/transport/local/connection.rb +14 -1
- data/lib/bolt/transport/orch.rb +12 -0
- data/lib/bolt/transport/simple.rb +6 -0
- data/lib/bolt/transport/ssh/connection.rb +9 -1
- data/lib/bolt/transport/ssh/exec_connection.rb +22 -1
- data/lib/bolt/transport/winrm/connection.rb +109 -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
- metadata +5 -2
data/lib/bolt/config.rb
CHANGED
@@ -499,7 +499,7 @@ module Bolt
|
|
499
499
|
end
|
500
500
|
|
501
501
|
def matching_paths(paths)
|
502
|
-
|
502
|
+
Array(paths).map { |p| Dir.glob([p, casefold(p)]) }.flatten.uniq.reject { |p| Array(paths).include?(p) }
|
503
503
|
end
|
504
504
|
|
505
505
|
private def casefold(path)
|
data/lib/bolt/config/options.rb
CHANGED
@@ -275,11 +275,25 @@ module Bolt
|
|
275
275
|
type: String,
|
276
276
|
_example: "/etc/puppetlabs/puppet/ssl/certs/my-host.example.com.pem"
|
277
277
|
},
|
278
|
+
"connect_timeout" => {
|
279
|
+
description: "How long to wait in seconds when establishing connections with PuppetDB.",
|
280
|
+
type: Integer,
|
281
|
+
minimum: 1,
|
282
|
+
_default: 60,
|
283
|
+
_example: 120
|
284
|
+
},
|
278
285
|
"key" => {
|
279
286
|
description: "The private key for the certificate.",
|
280
287
|
type: String,
|
281
288
|
_example: "/etc/puppetlabs/puppet/ssl/private_keys/my-host.example.com.pem"
|
282
289
|
},
|
290
|
+
"read_timeout" => {
|
291
|
+
description: "How long to wait in seconds for a response from PuppetDB.",
|
292
|
+
type: Integer,
|
293
|
+
minimum: 1,
|
294
|
+
_default: 60,
|
295
|
+
_example: 120
|
296
|
+
},
|
283
297
|
"server_urls" => {
|
284
298
|
description: "An array containing the PuppetDB host to connect to. Include the protocol `https` "\
|
285
299
|
"and the port, which is usually `8081`. For example, "\
|
data/lib/bolt/executor.rb
CHANGED
@@ -320,6 +320,21 @@ module Bolt
|
|
320
320
|
end
|
321
321
|
end
|
322
322
|
|
323
|
+
def download_file(targets, source, destination, options = {})
|
324
|
+
description = options.fetch(:description, "file download from #{source} to #{destination}")
|
325
|
+
FileUtils.mkdir_p(destination)
|
326
|
+
|
327
|
+
log_action(description, targets) do
|
328
|
+
options[:run_as] = run_as if run_as && !options.key?(:run_as)
|
329
|
+
|
330
|
+
batch_execute(targets) do |transport, batch|
|
331
|
+
with_node_logging("Downloading file #{source} to #{destination}", batch) do
|
332
|
+
transport.batch_download(batch, source, destination, options, &method(:publish_event))
|
333
|
+
end
|
334
|
+
end
|
335
|
+
end
|
336
|
+
end
|
337
|
+
|
323
338
|
def run_plan(scope, plan, params)
|
324
339
|
plan.call_by_name_with_scope(scope, params, true)
|
325
340
|
end
|
data/lib/bolt/inventory/group.rb
CHANGED
@@ -50,10 +50,11 @@ module Bolt
|
|
50
50
|
# or it could be a name/alias of a target defined in another group.
|
51
51
|
# We can't tell the difference until all groups have been resolved,
|
52
52
|
# so we store the string on its own here and process it later.
|
53
|
-
|
53
|
+
case target
|
54
|
+
when String
|
54
55
|
@string_targets << target
|
55
56
|
# Handle plugins at this level so that lookups cannot trigger recursive lookups
|
56
|
-
|
57
|
+
when Hash
|
57
58
|
add_target_definition(target)
|
58
59
|
else
|
59
60
|
raise ValidationError.new("Target entry must be a String or Hash, not #{target.class}", @name)
|
@@ -109,11 +109,12 @@ module Bolt
|
|
109
109
|
private :resolve_name
|
110
110
|
|
111
111
|
def expand_targets(targets)
|
112
|
-
|
112
|
+
case targets
|
113
|
+
when Bolt::Target
|
113
114
|
targets
|
114
|
-
|
115
|
+
when Array
|
115
116
|
targets.map { |tish| expand_targets(tish) }
|
116
|
-
|
117
|
+
when String
|
117
118
|
# Expand a comma-separated list
|
118
119
|
targets.split(/[[:space:],]+/).reject(&:empty?).map do |name|
|
119
120
|
ts = resolve_name(name)
|
data/lib/bolt/pal.rb
CHANGED
@@ -150,7 +150,12 @@ module Bolt
|
|
150
150
|
r = Puppet::Pal.in_tmp_environment('bolt', modulepath: @modulepath, facts: {}) do |pal|
|
151
151
|
# Only load the project if it a) exists, b) has a name it can be loaded with
|
152
152
|
bolt_project = @project if @project&.name
|
153
|
+
# Puppet currently won't receive the project unless it is a named project. Since
|
154
|
+
# the download_file plan function needs access to the project path, add it to the
|
155
|
+
# context.
|
156
|
+
bolt_project_data = @project
|
153
157
|
Puppet.override(bolt_project: bolt_project,
|
158
|
+
bolt_project_data: bolt_project_data,
|
154
159
|
yaml_plan_instantiator: Bolt::PAL::YamlPlan::Loader) do
|
155
160
|
pal.with_script_compiler do |compiler|
|
156
161
|
alias_types(compiler)
|
@@ -279,9 +284,10 @@ module Bolt
|
|
279
284
|
|
280
285
|
def parse_params(type, object_name, params)
|
281
286
|
in_bolt_compiler do |compiler|
|
282
|
-
|
287
|
+
case type
|
288
|
+
when 'task'
|
283
289
|
param_spec = compiler.task_signature(object_name)&.task_hash&.dig('parameters')
|
284
|
-
|
290
|
+
when 'plan'
|
285
291
|
plan = compiler.plan_signature(object_name)
|
286
292
|
param_spec = plan.params_type.elements&.each_with_object({}) { |t, h| h[t.name] = t.value_type } if plan
|
287
293
|
end
|
@@ -73,7 +73,7 @@ module Bolt
|
|
73
73
|
end
|
74
74
|
|
75
75
|
def upload_step(scope, step)
|
76
|
-
source = step['source']
|
76
|
+
source = step['upload'] || step['source']
|
77
77
|
destination = step['destination']
|
78
78
|
targets = step['targets'] || step['target']
|
79
79
|
description = step['description']
|
@@ -83,6 +83,17 @@ module Bolt
|
|
83
83
|
scope.call_function('upload_file', args)
|
84
84
|
end
|
85
85
|
|
86
|
+
def download_step(scope, step)
|
87
|
+
source = step['download']
|
88
|
+
destination = step['destination']
|
89
|
+
targets = step['targets'] || step['target']
|
90
|
+
description = step['description']
|
91
|
+
|
92
|
+
args = [source, destination, targets]
|
93
|
+
args << description if description
|
94
|
+
scope.call_function('download_file', args)
|
95
|
+
end
|
96
|
+
|
86
97
|
def eval_step(_scope, step)
|
87
98
|
step['eval']
|
88
99
|
end
|
@@ -145,6 +156,12 @@ module Bolt
|
|
145
156
|
Bolt::Logger.deprecation_warning("Using 'target' parameter for YAML plan steps, not 'targets'", msg)
|
146
157
|
end
|
147
158
|
|
159
|
+
if plan.steps.any? { |step| step.body.key?('source') }
|
160
|
+
msg = "The 'source' parameter for YAML plan upload steps is deprecated and will be removed "\
|
161
|
+
"in a future version of Bolt. Use the 'upload' parameter instead."
|
162
|
+
Bolt::Logger.deprecation_warning("Using 'source' parameter for YAML upload steps, not 'upload'", msg)
|
163
|
+
end
|
164
|
+
|
148
165
|
plan_result = closure_scope.with_local_scope(args_hash) do |scope|
|
149
166
|
plan.steps.each do |step|
|
150
167
|
step_result = dispatch_step(scope, step)
|
@@ -12,7 +12,7 @@ module Bolt
|
|
12
12
|
Set['name', 'description', 'target', 'targets']
|
13
13
|
end
|
14
14
|
|
15
|
-
STEP_KEYS = %w[command script task plan source destination eval resources].freeze
|
15
|
+
STEP_KEYS = %w[command script task plan source destination eval resources upload download].freeze
|
16
16
|
|
17
17
|
def self.create(step_body, step_number)
|
18
18
|
type_keys = (STEP_KEYS & step_body.keys)
|
@@ -22,8 +22,10 @@ module Bolt
|
|
22
22
|
when 1
|
23
23
|
type = type_keys.first
|
24
24
|
else
|
25
|
-
if
|
25
|
+
if [Set['source', 'destination'], Set['upload', 'destination']].include?(type_keys.to_set)
|
26
26
|
type = 'upload'
|
27
|
+
elsif type_keys.to_set == Set['download', 'destination']
|
28
|
+
type = 'download'
|
27
29
|
else
|
28
30
|
raise step_error("Multiple action keys detected: #{type_keys.inspect}", step_body['name'], step_number)
|
29
31
|
end
|
@@ -89,6 +91,12 @@ module Bolt
|
|
89
91
|
missing_keys -= ['targets']
|
90
92
|
end
|
91
93
|
|
94
|
+
# Handle cases where upload step uses deprecated 'source' key instead of 'upload'
|
95
|
+
# TODO: Remove when 'source' is removed
|
96
|
+
if body.include?('source')
|
97
|
+
missing_keys -= ['upload']
|
98
|
+
end
|
99
|
+
|
92
100
|
if missing_keys.any?
|
93
101
|
error_message = "The #{step_type.inspect} step requires: #{missing_keys.to_a.inspect} key(s)"
|
94
102
|
err = step_error(error_message, body['name'], step_number)
|
@@ -156,3 +164,4 @@ require 'bolt/pal/yaml_plan/step/resources'
|
|
156
164
|
require 'bolt/pal/yaml_plan/step/script'
|
157
165
|
require 'bolt/pal/yaml_plan/step/task'
|
158
166
|
require 'bolt/pal/yaml_plan/step/upload'
|
167
|
+
require 'bolt/pal/yaml_plan/step/download'
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Bolt
|
4
|
+
class PAL
|
5
|
+
class YamlPlan
|
6
|
+
class Step
|
7
|
+
class Download < Step
|
8
|
+
def self.allowed_keys
|
9
|
+
super + Set['download', 'destination']
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.required_keys
|
13
|
+
Set['download', 'destination', 'targets']
|
14
|
+
end
|
15
|
+
|
16
|
+
def initialize(step_body)
|
17
|
+
super
|
18
|
+
@source = step_body['download']
|
19
|
+
@destination = step_body['destination']
|
20
|
+
end
|
21
|
+
|
22
|
+
def transpile
|
23
|
+
code = String.new(" ")
|
24
|
+
code << "$#{@name} = " if @name
|
25
|
+
|
26
|
+
fn = 'download_file'
|
27
|
+
args = [@source, @destination, @targets]
|
28
|
+
args << @description if @description
|
29
|
+
|
30
|
+
code << function_call(fn, args)
|
31
|
+
|
32
|
+
code << "\n"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -6,16 +6,16 @@ module Bolt
|
|
6
6
|
class Step
|
7
7
|
class Upload < Step
|
8
8
|
def self.allowed_keys
|
9
|
-
super + Set['source', 'destination']
|
9
|
+
super + Set['source', 'destination', 'upload']
|
10
10
|
end
|
11
11
|
|
12
12
|
def self.required_keys
|
13
|
-
Set['
|
13
|
+
Set['upload', 'destination', 'targets']
|
14
14
|
end
|
15
15
|
|
16
16
|
def initialize(step_body)
|
17
17
|
super
|
18
|
-
@source = step_body['source']
|
18
|
+
@source = step_body['upload'] || step_body['source']
|
19
19
|
@destination = step_body['destination']
|
20
20
|
end
|
21
21
|
|
data/lib/bolt/plugin/puppetdb.rb
CHANGED
@@ -85,7 +85,8 @@ module Bolt
|
|
85
85
|
|
86
86
|
def resolve_facts(config, certname, target_data)
|
87
87
|
Bolt::Util.walk_vals(config) do |value|
|
88
|
-
|
88
|
+
case value
|
89
|
+
when String
|
89
90
|
if value == 'certname'
|
90
91
|
certname
|
91
92
|
else
|
@@ -94,7 +95,7 @@ module Bolt
|
|
94
95
|
# If there's no fact data this will be nil
|
95
96
|
data&.fetch('value', nil)
|
96
97
|
end
|
97
|
-
|
98
|
+
when Array, Hash
|
98
99
|
value
|
99
100
|
else
|
100
101
|
raise FactLookupError.new(value, "fact lookups must be a string")
|
data/lib/bolt/project.rb
CHANGED
@@ -17,7 +17,7 @@ module Bolt
|
|
17
17
|
|
18
18
|
attr_reader :path, :data, :config_file, :inventory_file, :modulepath, :hiera_config,
|
19
19
|
:puppetfile, :rerunfile, :type, :resource_types, :warnings, :project_file,
|
20
|
-
:deprecations
|
20
|
+
:deprecations, :downloads
|
21
21
|
|
22
22
|
def self.default_project
|
23
23
|
create_project(File.expand_path(File.join('~', '.puppetlabs', 'bolt')), 'user')
|
@@ -81,6 +81,7 @@ module Bolt
|
|
81
81
|
@rerunfile = @path + '.rerun.json'
|
82
82
|
@resource_types = @path + '.resource_types'
|
83
83
|
@type = type
|
84
|
+
@downloads = @path + 'downloads'
|
84
85
|
|
85
86
|
tc = Bolt::Config::INVENTORY_OPTIONS.keys & raw_data.keys
|
86
87
|
if tc.any?
|
data/lib/bolt/puppetdb/client.rb
CHANGED
@@ -96,6 +96,8 @@ module Bolt
|
|
96
96
|
@http = HTTPClient.new
|
97
97
|
@http.ssl_config.set_client_cert_file(@config.cert, @config.key) if @config.cert
|
98
98
|
@http.ssl_config.add_trust_ca(@config.cacert)
|
99
|
+
@http.connect_timeout = @config.connect_timeout if @config.connect_timeout
|
100
|
+
@http.receive_timeout = @config.read_timeout if @config.read_timeout
|
99
101
|
|
100
102
|
@http
|
101
103
|
end
|
data/lib/bolt/puppetdb/config.rb
CHANGED
@@ -132,6 +132,22 @@ module Bolt
|
|
132
132
|
end
|
133
133
|
end
|
134
134
|
|
135
|
+
def connect_timeout
|
136
|
+
validate_timeout('connect_timeout')
|
137
|
+
@settings['connect_timeout']
|
138
|
+
end
|
139
|
+
|
140
|
+
def read_timeout
|
141
|
+
validate_timeout('read_timeout')
|
142
|
+
@settings['read_timeout']
|
143
|
+
end
|
144
|
+
|
145
|
+
def validate_timeout(timeout)
|
146
|
+
unless @settings[timeout].nil? || (@settings[timeout].is_a?(Integer) && @settings[timeout] > 0)
|
147
|
+
raise Bolt::PuppetDBError, "#{timeout} must be a positive integer, received #{@settings[timeout]}"
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
135
151
|
def to_hash
|
136
152
|
@settings.dup
|
137
153
|
end
|
data/lib/bolt/result.rb
CHANGED
@@ -79,6 +79,13 @@ module Bolt
|
|
79
79
|
new(target, message: "Uploaded '#{source}' to '#{target.host}:#{destination}'", action: 'upload', object: source)
|
80
80
|
end
|
81
81
|
|
82
|
+
def self.for_download(target, source, destination, download)
|
83
|
+
msg = "Downloaded '#{target.host}:#{source}' to '#{destination}'"
|
84
|
+
value = { 'path' => download }
|
85
|
+
|
86
|
+
new(target, value: value, message: msg, action: 'download', object: source)
|
87
|
+
end
|
88
|
+
|
82
89
|
# Satisfies the Puppet datatypes API
|
83
90
|
def self.from_asserted_args(target, value)
|
84
91
|
new(target, value: value)
|
data/lib/bolt/shell/bash.rb
CHANGED
@@ -37,7 +37,7 @@ module Bolt
|
|
37
37
|
with_tmpdir do |dir|
|
38
38
|
basename = File.basename(source)
|
39
39
|
tmpfile = File.join(dir.to_s, basename)
|
40
|
-
conn.
|
40
|
+
conn.upload_file(source, tmpfile)
|
41
41
|
# pass over file ownership if we're using run-as to be a different user
|
42
42
|
dir.chown(run_as)
|
43
43
|
result = execute(['mv', '-f', tmpfile, destination], sudoable: true)
|
@@ -50,6 +50,27 @@ module Bolt
|
|
50
50
|
end
|
51
51
|
end
|
52
52
|
|
53
|
+
def download(source, destination, options = {})
|
54
|
+
running_as(options[:run_as]) do
|
55
|
+
# Target OS may be either Unix or Windows. Without knowing the target OS before-hand
|
56
|
+
# we can't assume whether the path separator is '/' or '\'. Assume we're connecting
|
57
|
+
# to a target with Unix and then check if the path exists after downloading.
|
58
|
+
download = File.join(destination, Bolt::Util.unix_basename(source))
|
59
|
+
|
60
|
+
conn.download_file(source, destination, download)
|
61
|
+
|
62
|
+
# If the download path doesn't exist, then the file was likely downloaded from Windows
|
63
|
+
# using a source path with backslashes (e.g. 'C:\Users\Administrator\foo'). The file
|
64
|
+
# should be saved to the expected location, so update the download path assuming a
|
65
|
+
# Windows basename so the result shows the correct local path.
|
66
|
+
unless File.exist?(download)
|
67
|
+
download = File.join(destination, Bolt::Util.windows_basename(source))
|
68
|
+
end
|
69
|
+
|
70
|
+
Bolt::Result.for_download(target, source, destination, download)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
53
74
|
def run_script(script, arguments, options = {})
|
54
75
|
# unpack any Sensitive data
|
55
76
|
arguments = unwrap_sensitive_args(arguments)
|
@@ -95,7 +116,7 @@ module Bolt
|
|
95
116
|
task_dir = File.join(dir.to_s, task.tasks_dir)
|
96
117
|
dir.mkdirs([task.tasks_dir] + extra_files.map { |file| File.dirname(file['name']) })
|
97
118
|
extra_files.each do |file|
|
98
|
-
conn.
|
119
|
+
conn.upload_file(file['path'], File.join(dir.to_s, file['name']))
|
99
120
|
end
|
100
121
|
end
|
101
122
|
|
@@ -257,7 +278,7 @@ module Bolt
|
|
257
278
|
def write_executable(dir, file, filename = nil)
|
258
279
|
filename ||= File.basename(file)
|
259
280
|
remote_path = File.join(dir.to_s, filename)
|
260
|
-
conn.
|
281
|
+
conn.upload_file(file, remote_path)
|
261
282
|
make_executable(remote_path)
|
262
283
|
remote_path
|
263
284
|
end
|
@@ -314,7 +335,6 @@ module Bolt
|
|
314
335
|
sudo_str = if use_sudo
|
315
336
|
sudo_exec = target.options['sudo-executable'] || "sudo"
|
316
337
|
sudo_flags = [sudo_exec, "-S", "-H", "-u", run_as, "-p", sudo_prompt]
|
317
|
-
sudo_flags += ["-E"] if options[:environment]
|
318
338
|
Shellwords.shelljoin(sudo_flags)
|
319
339
|
else
|
320
340
|
Shellwords.shelljoin(@target.options['run-as-command'] + [run_as])
|
@@ -85,7 +85,7 @@ module Bolt
|
|
85
85
|
filename ||= File.basename(file)
|
86
86
|
validate_extensions(File.extname(filename))
|
87
87
|
destination = "#{dir}\\#{filename}"
|
88
|
-
conn.
|
88
|
+
conn.upload_file(file, destination)
|
89
89
|
destination
|
90
90
|
end
|
91
91
|
|
@@ -164,10 +164,16 @@ module Bolt
|
|
164
164
|
end
|
165
165
|
|
166
166
|
def upload(source, destination, _options = {})
|
167
|
-
conn.
|
167
|
+
conn.upload_file(source, destination)
|
168
168
|
Bolt::Result.for_upload(target, source, destination)
|
169
169
|
end
|
170
170
|
|
171
|
+
def download(source, destination, _options = {})
|
172
|
+
download = File.join(destination, Bolt::Util.windows_basename(source))
|
173
|
+
conn.download_file(source, destination, download)
|
174
|
+
Bolt::Result.for_download(target, source, destination, download)
|
175
|
+
end
|
176
|
+
|
171
177
|
def run_command(command, options = {})
|
172
178
|
command = [*env_declarations(options[:env_vars]), command].join("\r\n") if options[:env_vars]
|
173
179
|
|
@@ -220,7 +226,7 @@ module Bolt
|
|
220
226
|
task_dir = File.join(dir, task.tasks_dir)
|
221
227
|
mkdirs([task_dir] + extra_files.map { |file| File.join(dir, File.dirname(file['name'])) })
|
222
228
|
extra_files.each do |file|
|
223
|
-
conn.
|
229
|
+
conn.upload_file(file['path'], File.join(dir, file['name']))
|
224
230
|
end
|
225
231
|
end
|
226
232
|
|
@@ -262,7 +268,7 @@ module Bolt
|
|
262
268
|
return with_tmpdir do |dir|
|
263
269
|
command += "\r\nif (!$?) { if($LASTEXITCODE) { exit $LASTEXITCODE } else { exit 1 } }"
|
264
270
|
script_file = File.join(dir, "#{SecureRandom.uuid}_wrapper.ps1")
|
265
|
-
conn.
|
271
|
+
conn.upload_file(StringIO.new(command), script_file)
|
266
272
|
args = escape_arguments([script_file])
|
267
273
|
script_invocation = ['powershell.exe', *PS_ARGS, '-File', *args].join(' ')
|
268
274
|
execute(script_invocation)
|