bolt 1.5.0 → 1.6.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 59ccedf25067dbbeb8769b4cb77178679c6b5a7a312dcecba80841af2d472ffe
4
- data.tar.gz: 358e3ce1e0e4e3241eb9442fac18b80b9e36bec98b3e3d1dbbd039c5708d458b
3
+ metadata.gz: 557353adda3a863e4f748933cb24860616848d1696d9f0914be23474616f43ca
4
+ data.tar.gz: 91dc4d72ba25e2e827c5bf170574bedef50c744a4e31ac48684699ff59cdf753
5
5
  SHA512:
6
- metadata.gz: 42734205200d757dd20e82aeed34f61734b4688fd8e72de40b5f5341fdd1f452537de9db1277b585a9cf274abb0dad0ba20cc2a5822e4769d40c505ca42da4f5
7
- data.tar.gz: 6c906fafb671ab787e79fbcdb3bfbd544b31eeb2ade0f9937be4c3ebe7472dc9d7a399ed5ae8e357d2582e93250371a15beadf08d93cd9a68762e85e91dc9088
6
+ metadata.gz: 6a4c8b39f2c353498168cf89a5e70f7b4699e6332e2826efd24a905d94ca7b52ec76a6db98e94f83fe5f7fce4b6c7f58cbc51b56e79b3fce3acf0f014f2669f8
7
+ data.tar.gz: 6bf22f4aaef6a2741b64d77b900c9a85fd3c3588feb4a071d582415afbc5ecc479048ba7aea9912131c383d5ec2e3966682f5ea8a3cade87722a927d12275c7e
@@ -99,7 +99,7 @@ Puppet::Functions.create_function(:run_task) do
99
99
  options['_description'] = description if description
100
100
 
101
101
  # Don't bother loading the local task definition if all targets use the 'pcp' transport.
102
- if !targets.empty? && targets.all? { |t| t.protocol == 'pcp' }
102
+ if !targets.empty? && targets.all? { |t| t.transport == 'pcp' }
103
103
  # create a fake task
104
104
  task = Bolt::Task.new(name: task_name, files: [{ 'name' => '', 'path' => '' }])
105
105
  else
@@ -10,6 +10,7 @@ require 'bolt/transport/winrm'
10
10
  require 'bolt/transport/orch'
11
11
  require 'bolt/transport/local'
12
12
  require 'bolt/transport/docker'
13
+ require 'bolt/transport/remote'
13
14
 
14
15
  module Bolt
15
16
  TRANSPORTS = {
@@ -17,7 +18,8 @@ module Bolt
17
18
  winrm: Bolt::Transport::WinRM,
18
19
  pcp: Bolt::Transport::Orch,
19
20
  local: Bolt::Transport::Local,
20
- docker: Bolt::Transport::Docker
21
+ docker: Bolt::Transport::Docker,
22
+ remote: Bolt::Transport::Remote
21
23
  }.freeze
22
24
 
23
25
  class UnknownTransportError < Bolt::Error
@@ -36,16 +38,15 @@ module Bolt
36
38
  private-key tty tmpdir user connect-timeout
37
39
  cacert token-file service-url].freeze
38
40
 
39
- TRANSPORT_DEFAULTS = {
40
- 'connect-timeout' => 10,
41
- 'tty' => false
42
- }.freeze
43
-
41
+ # TODO: move these to the transport themselves
44
42
  TRANSPORT_SPECIFIC_DEFAULTS = {
45
43
  ssh: {
46
- 'host-key-check' => true
44
+ 'connect-timeout' => 10,
45
+ 'host-key-check' => true,
46
+ 'tty' => false
47
47
  },
48
48
  winrm: {
49
+ 'connect-timeout' => 10,
49
50
  'ssl' => true,
50
51
  'ssl-verify' => true
51
52
  },
@@ -53,7 +54,10 @@ module Bolt
53
54
  'task-environment' => 'production'
54
55
  },
55
56
  local: {},
56
- docker: {}
57
+ docker: {},
58
+ remote: {
59
+ 'run-on' => 'localhost'
60
+ }
57
61
  }.freeze
58
62
 
59
63
  def self.default
@@ -88,7 +92,7 @@ module Bolt
88
92
 
89
93
  @transports = {}
90
94
  TRANSPORTS.each_key do |transport|
91
- @transports[transport] = TRANSPORT_DEFAULTS.merge(TRANSPORT_SPECIFIC_DEFAULTS[transport])
95
+ @transports[transport] = TRANSPORT_SPECIFIC_DEFAULTS[transport].dup
92
96
  end
93
97
 
94
98
  update_from_file(config_data)
@@ -158,7 +162,7 @@ module Bolt
158
162
 
159
163
  TRANSPORTS.each do |key, impl|
160
164
  if data[key.to_s]
161
- selected = data[key.to_s].select { |k| impl.options.include?(k) }
165
+ selected = impl.filter_options(data[key.to_s])
162
166
  @transports[key].merge!(selected)
163
167
  end
164
168
  end
@@ -32,9 +32,15 @@ module Bolt
32
32
  @load_config = load_config
33
33
 
34
34
  @transports = Bolt::TRANSPORTS.each_with_object({}) do |(key, val), coll|
35
- coll[key.to_s] = Concurrent::Delay.new do
36
- val.new
37
- end
35
+ coll[key.to_s] = if key == :remote
36
+ Concurrent::Delay.new do
37
+ val.new(self)
38
+ end
39
+ else
40
+ Concurrent::Delay.new do
41
+ val.new
42
+ end
43
+ end
38
44
  end
39
45
  @reported_transports = Set.new
40
46
 
@@ -64,7 +70,7 @@ module Bolt
64
70
  # defined by the transport. Yields each batch, along with the corresponding
65
71
  # transport, to the block in turn and returns an array of result promises.
66
72
  def queue_execute(targets)
67
- targets.group_by(&:protocol).flat_map do |protocol, protocol_targets|
73
+ targets.group_by(&:transport).flat_map do |protocol, protocol_targets|
68
74
  transport = transport(protocol)
69
75
  report_transport(transport, protocol_targets.count)
70
76
  transport.batches(protocol_targets).flat_map do |batch|
@@ -167,19 +167,17 @@ module Bolt
167
167
  # Should this reconfigure configured targets?
168
168
  def update_target(target)
169
169
  data = @groups.data_for(target.name)
170
-
171
- unless data
172
- data = {}
173
- unless Bolt::Util.windows?
174
- data['config'] = { 'transport' => 'local' } if target.name == 'localhost'
175
- end
176
- end
170
+ data ||= {}
177
171
 
178
172
  unless data['config']
179
173
  @logger.debug("Did not find config for #{target.name} in inventory")
180
174
  data['config'] = {}
181
175
  end
182
176
 
177
+ unless data['config']['transport']
178
+ data['config']['transport'] = 'local' if target.name == 'localhost'
179
+ end
180
+
183
181
  # These should only get set from the inventory if they have not yet
184
182
  # been instantiated
185
183
  set_vars_from_hash(target.name, data['vars']) unless @target_vars[target.name]
@@ -191,11 +189,13 @@ module Bolt
191
189
  conf.update_from_inventory(data['config'])
192
190
  conf.validate
193
191
 
194
- unless target.protocol.nil? || Bolt::TRANSPORTS.include?(target.protocol.to_sym)
195
- raise Bolt::UnknownTransportError.new(target.protocol, target.uri)
192
+ target.update_conf(conf.transport_conf)
193
+
194
+ unless target.transport.nil? || Bolt::TRANSPORTS.include?(target.transport.to_sym)
195
+ raise Bolt::UnknownTransportError.new(target.transport, target.uri)
196
196
  end
197
197
 
198
- target.update_conf(conf.transport_conf)
198
+ target
199
199
  end
200
200
  private :update_target
201
201
 
@@ -226,6 +226,7 @@ module Bolt
226
226
 
227
227
  def expand_targets(targets)
228
228
  if targets.is_a? Bolt::Target
229
+ targets.inventory = self
229
230
  targets
230
231
  elsif targets.is_a? Array
231
232
  targets.map { |tish| expand_targets(tish) }
@@ -7,7 +7,8 @@ module Bolt
7
7
  class BaseError < Bolt::Error
8
8
  attr_reader :issue_code
9
9
 
10
- def initialize(message, issue_code)
10
+ # TODO: can we just drop issue code here?
11
+ def initialize(message, issue_code = nil)
11
12
  super(message, kind, nil, issue_code)
12
13
  end
13
14
 
@@ -34,6 +35,12 @@ module Bolt
34
35
  end
35
36
  end
36
37
 
38
+ class RemoteError < BaseError
39
+ def kind
40
+ 'puppetlabs.tasks/remote-task-error'
41
+ end
42
+ end
43
+
37
44
  class EnvironmentVarError < BaseError
38
45
  def initialize(var, val)
39
46
  message = "Could not set environment variable '#{var}' to '#{val}'"
@@ -6,7 +6,9 @@ require 'bolt/error'
6
6
  module Bolt
7
7
  class Target
8
8
  attr_reader :uri, :options
9
- attr_writer :inventory
9
+ # CODEREVIEW: this feels wrong. The altertative is threading inventory through the
10
+ # executor to the RemoteTransport
11
+ attr_accessor :inventory
10
12
 
11
13
  PRINT_OPTS ||= %w[host user port protocol].freeze
12
14
 
@@ -41,7 +43,7 @@ module Bolt
41
43
  def update_conf(conf)
42
44
  @protocol = conf[:transport]
43
45
 
44
- t_conf = conf[:transports][protocol.to_sym]
46
+ t_conf = conf[:transports][transport.to_sym] || {}
45
47
  # Override url methods
46
48
  @user = t_conf['user']
47
49
  @password = t_conf['password']
@@ -85,6 +87,18 @@ module Bolt
85
87
  "Target('#{@uri}', #{opts})"
86
88
  end
87
89
 
90
+ def to_h
91
+ options.merge(
92
+ 'name' => name,
93
+ 'uri' => uri,
94
+ 'protocol' => protocol,
95
+ 'user' => user,
96
+ 'password' => password,
97
+ 'host' => host,
98
+ 'port' => port
99
+ )
100
+ end
101
+
88
102
  def host
89
103
  @uri_obj.hostname
90
104
  end
@@ -95,10 +109,19 @@ module Bolt
95
109
  uri
96
110
  end
97
111
 
112
+ def remote?
113
+ @uri_obj.scheme == 'remote' || @protocol == 'remote'
114
+ end
115
+
98
116
  def port
99
117
  @uri_obj.port || @port
100
118
  end
101
119
 
120
+ # transport is separate from protocol for remote targets.
121
+ def transport
122
+ remote? ? 'remote' : protocol
123
+ end
124
+
102
125
  def protocol
103
126
  @uri_obj.scheme || @protocol
104
127
  end
@@ -1,6 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Bolt
4
+ class NoImplementationError < Bolt::Error
5
+ def initialize(target, task)
6
+ msg = "No suitable implementation of #{task.name} for #{target.name}"
7
+ super(msg, 'bolt/no-implementation')
8
+ end
9
+ end
10
+
4
11
  # Represents a Task.
5
12
  # @file and @files are mutually exclusive.
6
13
  # @name [String] name of the task
@@ -51,13 +58,17 @@ module Bolt
51
58
  file_map[file_name]['path']
52
59
  end
53
60
 
61
+ def implementations
62
+ metadata['implementations']
63
+ end
64
+
54
65
  # Returns a hash of implementation name, path to executable, input method (if defined),
55
66
  # and any additional files (name and path)
56
67
  def select_implementation(target, additional_features = [])
57
- impl = if (impls = metadata['implementations'])
68
+ impl = if (impls = implementations)
58
69
  available_features = target.features + additional_features
59
70
  impl = impls.find { |imp| Set.new(imp['requirements']).subset?(available_features) }
60
- raise "No suitable implementation of #{name} for #{target.name}" unless impl
71
+ raise NoImplementationError.new(target, self) unless impl
61
72
  impl = impl.dup
62
73
  impl['path'] = file_path(impl['name'])
63
74
  impl.delete('requirements')
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bolt/task'
4
+
5
+ module Bolt
6
+ class Task
7
+ class Remote < Task
8
+ def self.from_task(task)
9
+ new(task.name, task.file, task.files, task.metadata)
10
+ end
11
+
12
+ def implementations
13
+ metadata['implementations']&.select { |i| i['remote'] || metadata['remote'] }
14
+ end
15
+
16
+ def select_implementation(target, *args)
17
+ unless implementations || metadata['remote']
18
+ raise NoImplementationError.new(target, self)
19
+ end
20
+
21
+ super(target, *args)
22
+ end
23
+ end
24
+ end
25
+ end
@@ -44,7 +44,12 @@ module Bolt
44
44
 
45
45
  # Returns options this transport supports
46
46
  def self.options
47
- raise NotImplementedError, "self.options() must be implemented by the transport class"
47
+ raise NotImplementedError,
48
+ "self.options() or self.filter_options(unfiltered) must be implemented by the transport class"
49
+ end
50
+
51
+ def self.filter_options(unfiltered)
52
+ unfiltered.select { |k| options.include?(k) }
48
53
  end
49
54
 
50
55
  def self.validate(_options)
@@ -72,13 +77,24 @@ module Bolt
72
77
  []
73
78
  end
74
79
 
75
- def filter_options(target, options)
80
+ def default_input_method(_executable)
81
+ 'both'
82
+ end
83
+
84
+ def select_implementation(target, task)
85
+ impl = task.select_implementation(target, provided_features)
86
+ impl['input_method'] ||= default_input_method(impl['path'])
87
+ impl
88
+ end
89
+
90
+ def reject_transport_options(target, options)
76
91
  if target.options['run-as']
77
92
  options.reject { |k, _v| k == '_run_as' }
78
93
  else
79
94
  options
80
95
  end
81
96
  end
97
+ private :reject_transport_options
82
98
 
83
99
  # Transform a parameter map to an environment variable map, with parameter names prefixed
84
100
  # with 'PT_' and values transformed to JSON unless they're strings.
@@ -111,7 +127,7 @@ module Bolt
111
127
  target = targets.first
112
128
  with_events(target, callback) do
113
129
  @logger.debug { "Running task run '#{task}' on #{target.uri}" }
114
- run_task(target, task, arguments, filter_options(target, options))
130
+ run_task(target, task, arguments, reject_transport_options(target, options))
115
131
  end
116
132
  end
117
133
 
@@ -125,7 +141,7 @@ module Bolt
125
141
  target = targets.first
126
142
  with_events(target, callback) do
127
143
  @logger.debug("Running command '#{command}' on #{target.uri}")
128
- run_command(target, command, filter_options(target, options))
144
+ run_command(target, command, reject_transport_options(target, options))
129
145
  end
130
146
  end
131
147
 
@@ -139,7 +155,7 @@ module Bolt
139
155
  target = targets.first
140
156
  with_events(target, callback) do
141
157
  @logger.debug { "Running script '#{script}' on #{target.uri}" }
142
- run_script(target, script, arguments, filter_options(target, options))
158
+ run_script(target, script, arguments, reject_transport_options(target, options))
143
159
  end
144
160
  end
145
161
 
@@ -153,7 +169,7 @@ module Bolt
153
169
  target = targets.first
154
170
  with_events(target, callback) do
155
171
  @logger.debug { "Uploading: '#{source}' to #{destination} on #{target.uri}" }
156
- upload(target, source, destination, filter_options(target, options))
172
+ upload(target, source, destination, reject_transport_options(target, options))
157
173
  end
158
174
  end
159
175
 
@@ -116,6 +116,12 @@ module Bolt
116
116
  end
117
117
  end
118
118
  end
119
+
120
+ def connected?(target)
121
+ with_connection(target) { true }
122
+ rescue Bolt::Node::ConnectError
123
+ false
124
+ end
119
125
  end
120
126
  end
121
127
  end
@@ -4,6 +4,7 @@ require 'json'
4
4
  require 'fileutils'
5
5
  require 'tmpdir'
6
6
  require 'bolt/transport/base'
7
+ require 'bolt/transport/powershell'
7
8
  require 'bolt/util'
8
9
 
9
10
  module Bolt
@@ -14,19 +15,23 @@ module Bolt
14
15
  end
15
16
 
16
17
  def provided_features
17
- ['shell']
18
+ if Bolt::Util.windows?
19
+ ['powershell']
20
+ else
21
+ ['shell']
22
+ end
23
+ end
24
+
25
+ def default_input_method(executable)
26
+ input_method ||= Powershell.powershell_file?(executable) ? 'powershell' : 'both'
27
+ input_method
18
28
  end
19
29
 
20
30
  def self.validate(_options); end
21
31
 
22
32
  def initialize
23
33
  super
24
-
25
- if Bolt::Util.windows?
26
- raise NotImplementedError, "The local transport is not yet implemented on Windows"
27
- else
28
- @conn = Shell.new
29
- end
34
+ @conn = Shell.new
30
35
  end
31
36
 
32
37
  def in_tmpdir(base)
@@ -73,20 +78,32 @@ module Bolt
73
78
 
74
79
  # unpack any Sensitive data AFTER we log
75
80
  arguments = unwrap_sensitive_args(arguments)
76
- if arguments.empty?
77
- # We will always provide separated arguments, so work-around Open3's handling of a single
78
- # argument as the entire command string for script paths containing spaces.
79
- arguments = ['']
81
+ if Bolt::Util.windows?
82
+ if Powershell.powershell_file?(file)
83
+ command = Powershell.run_script(arguments, file)
84
+ output = @conn.execute(command, dir: dir, env: "powershell.exe")
85
+ else
86
+ path, args = *Powershell.process_from_extension(file)
87
+ args += Powershell.escape_arguments(arguments)
88
+ command = args.unshift(path).join(' ')
89
+ output = @conn.execute(command, dir: dir)
90
+ end
91
+ else
92
+ if arguments.empty?
93
+ # We will always provide separated arguments, so work-around Open3's handling of a single
94
+ # argument as the entire command string for script paths containing spaces.
95
+ arguments = ['']
96
+ end
97
+ output = @conn.execute(file, *arguments, dir: dir)
80
98
  end
81
- output = @conn.execute(file, *arguments, dir: dir)
82
99
  Bolt::Result.for_command(target, output.stdout.string, output.stderr.string, output.exit_code)
83
100
  end
84
101
  end
85
102
 
86
103
  def run_task(target, task, arguments, _options = {})
87
- implementation = task.select_implementation(target, provided_features)
104
+ implementation = select_implementation(target, task)
88
105
  executable = implementation['path']
89
- input_method = implementation['input_method'] || 'both'
106
+ input_method = implementation['input_method']
90
107
  extra_files = implementation['files']
91
108
 
92
109
  in_tmpdir(target.options['tmpdir']) do |dir|
@@ -108,16 +125,43 @@ module Bolt
108
125
  copy_file(executable, script)
109
126
  File.chmod(0o750, script)
110
127
 
111
- # unpack any Sensitive data, write it to a separate variable because
112
- # we log 'arguments' below
128
+ # log the arguments with sensitive data redacted, do NOT log unwrapped_arguments
129
+ logger.debug("Running '#{script}' with #{arguments}")
113
130
  unwrapped_arguments = unwrap_sensitive_args(arguments)
131
+
114
132
  stdin = STDIN_METHODS.include?(input_method) ? JSON.dump(unwrapped_arguments) : nil
115
- env = ENVIRONMENT_METHODS.include?(input_method) ? envify_params(unwrapped_arguments) : nil
116
133
 
117
- # log the arguments with sensitive data redacted, do NOT log unwrapped_arguments
118
- logger.debug("Running '#{script}' with #{arguments}")
134
+ if Bolt::Util.windows?
135
+ # WINDOWS
136
+ if ENVIRONMENT_METHODS.include?(input_method)
137
+ environment_params = envify_params(unwrapped_arguments).each_with_object([]) do |(arg, val), list|
138
+ list << Powershell.set_env(arg, val)
139
+ end
140
+ environment_params = environment_params.join("\n") + "\n"
141
+ else
142
+ environment_params = ""
143
+ end
119
144
 
120
- output = @conn.execute(script, stdin: stdin, env: env, dir: dir)
145
+ output =
146
+ if Powershell.powershell_file?(script) && stdin.nil?
147
+ command = Powershell.run_ps_task(arguments, script, input_method)
148
+ command = environment_params + Powershell.shell_init + command
149
+ if input_method == 'powershell'
150
+ @conn.execute(command, dir: dir, env: "powershell.exe")
151
+ else
152
+ @conn.execute(command, dir: dir, stdin: stdin, env: "powershell.exe")
153
+ end
154
+ else
155
+ path, args = *Powershell.process_from_extension(script)
156
+ command = args.unshift(path).join(' ')
157
+ command = environment_params + Powershell.shell_init + command
158
+ @conn.execute(command, dir: dir, stdin: stdin, env: "powershell.exe")
159
+ end
160
+ else
161
+ # POSIX
162
+ env = ENVIRONMENT_METHODS.include?(input_method) ? envify_params(unwrapped_arguments) : nil
163
+ output = @conn.execute(script, stdin: stdin, env: env, dir: dir)
164
+ end
121
165
  Bolt::Result.for_task(target, output.stdout.string, output.stderr.string, output.exit_code)
122
166
  end
123
167
  end