bolt 0.16.4 → 0.17.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.

Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/bolt-modules/boltlib/lib/puppet/functions/fail_plan.rb +2 -3
  3. data/bolt-modules/boltlib/lib/puppet/functions/file_upload.rb +2 -2
  4. data/bolt-modules/boltlib/lib/puppet/functions/get_targets.rb +2 -3
  5. data/bolt-modules/boltlib/lib/puppet/functions/run_command.rb +2 -2
  6. data/bolt-modules/boltlib/lib/puppet/functions/run_plan.rb +2 -2
  7. data/bolt-modules/boltlib/lib/puppet/functions/run_task.rb +2 -2
  8. data/bolt-modules/boltlib/lib/puppet/functions/set_var.rb +29 -0
  9. data/bolt-modules/boltlib/lib/puppet/functions/vars.rb +27 -0
  10. data/lib/bolt/cli.rb +220 -184
  11. data/lib/bolt/config.rb +95 -13
  12. data/lib/bolt/executor.rb +12 -5
  13. data/lib/bolt/inventory.rb +48 -11
  14. data/lib/bolt/inventory/group.rb +22 -2
  15. data/lib/bolt/logger.rb +56 -8
  16. data/lib/bolt/outputter/human.rb +18 -1
  17. data/lib/bolt/pal.rb +6 -1
  18. data/lib/bolt/target.rb +1 -1
  19. data/lib/bolt/transport/base.rb +3 -0
  20. data/lib/bolt/transport/local.rb +90 -0
  21. data/lib/bolt/transport/local/shell.rb +29 -0
  22. data/lib/bolt/transport/orch.rb +2 -2
  23. data/lib/bolt/transport/ssh.rb +0 -3
  24. data/lib/bolt/transport/winrm.rb +0 -3
  25. data/lib/bolt/util.rb +31 -4
  26. data/lib/bolt/version.rb +1 -1
  27. data/lib/bolt_ext/puppetdb_inventory.rb +9 -2
  28. data/modules/aggregate/lib/puppet/functions/aggregate/count.rb +19 -0
  29. data/modules/aggregate/lib/puppet/functions/aggregate/nodes.rb +19 -0
  30. data/modules/aggregate/plans/count.pp +35 -0
  31. data/modules/aggregate/plans/nodes.pp +35 -0
  32. data/modules/canary/lib/puppet/functions/canary/merge.rb +11 -0
  33. data/modules/canary/lib/puppet/functions/canary/random_split.rb +20 -0
  34. data/modules/canary/lib/puppet/functions/canary/skip.rb +23 -0
  35. data/modules/canary/plans/init.pp +52 -0
  36. metadata +14 -16
@@ -3,11 +3,14 @@ require 'bolt/cli'
3
3
  require 'logging'
4
4
 
5
5
  module Bolt
6
+ TRANSPORTS = %i[ssh winrm pcp local].freeze
7
+
6
8
  Config = Struct.new(
7
9
  :concurrency,
8
10
  :format,
9
11
  :inventoryfile,
10
12
  :log_level,
13
+ :log,
11
14
  :modulepath,
12
15
  :transport,
13
16
  :transports
@@ -16,16 +19,16 @@ module Bolt
16
19
  DEFAULTS = {
17
20
  concurrency: 100,
18
21
  transport: 'ssh',
19
- format: 'human'
22
+ format: 'human',
23
+ modulepath: []
20
24
  }.freeze
21
25
 
22
26
  TRANSPORT_OPTIONS = %i[host_key_check password run_as sudo_password extensions
23
27
  ssl key tty tmpdir user connect_timeout cacert
24
- token_file orch_task_environment service_url].freeze
28
+ token-file task-environment service-url].freeze
25
29
 
26
30
  TRANSPORT_DEFAULTS = {
27
31
  connect_timeout: 10,
28
- orch_task_environment: 'production',
29
32
  tty: false
30
33
  }.freeze
31
34
 
@@ -36,16 +39,21 @@ module Bolt
36
39
  winrm: {
37
40
  ssl: true
38
41
  },
39
- pcp: {}
42
+ pcp: {
43
+ :"task-environment" => 'production'
44
+ },
45
+ local: {}
40
46
  }.freeze
41
47
 
42
- TRANSPORTS = %i[ssh winrm pcp].freeze
43
-
44
48
  def initialize(**kwargs)
45
49
  super()
46
50
  @logger = Logging.logger[self]
47
51
  DEFAULTS.merge(kwargs).each { |k, v| self[k] = v }
48
52
 
53
+ # add an entry for the default console logger
54
+ self[:log] ||= {}
55
+ self[:log]['console'] ||= {}
56
+
49
57
  self[:transports] ||= {}
50
58
  TRANSPORTS.each do |transport|
51
59
  unless self[:transports][transport]
@@ -65,12 +73,38 @@ module Bolt
65
73
  end
66
74
  end
67
75
 
76
+ def deep_clone
77
+ Bolt::Util.deep_clone(self)
78
+ end
79
+
68
80
  def default_paths
69
81
  root_path = File.expand_path(File.join('~', '.puppetlabs'))
70
82
  [File.join(root_path, 'bolt.yaml'), File.join(root_path, 'bolt.yml')]
71
83
  end
72
84
 
85
+ def normalize_log(target)
86
+ return target if target == 'console'
87
+ target = target[5..-1] if target.start_with?('file:')
88
+ 'file:' << File.expand_path(target)
89
+ end
90
+
73
91
  def update_from_file(data)
92
+ if data['log'].is_a?(Hash)
93
+ data['log'].each_pair do |k, v|
94
+ log = (self[:log][normalize_log(k)] ||= {})
95
+
96
+ next unless v.is_a?(Hash)
97
+
98
+ if v.key?('level')
99
+ log[:level] = v['level'].to_s
100
+ end
101
+
102
+ if v.key?('append')
103
+ log[:append] = v['append']
104
+ end
105
+ end
106
+ end
107
+
74
108
  if data['modulepath']
75
109
  self[:modulepath] = data['modulepath'].split(File::PATH_SEPARATOR)
76
110
  end
@@ -84,7 +118,7 @@ module Bolt
84
118
  end
85
119
 
86
120
  if data['format']
87
- self[:format] = data['format'] if data['format']
121
+ self[:format] = data['format']
88
122
  end
89
123
 
90
124
  if data['ssh']
@@ -127,19 +161,26 @@ module Bolt
127
161
 
128
162
  if data['pcp']
129
163
  if data['pcp']['service-url']
130
- self[:transports][:pcp][:service_url] = data['pcp']['service-url']
164
+ self[:transports][:pcp][:"service-url"] = data['pcp']['service-url']
131
165
  end
132
166
  if data['pcp']['cacert']
133
167
  self[:transports][:pcp][:cacert] = data['pcp']['cacert']
134
168
  end
135
169
  if data['pcp']['token-file']
136
- self[:transports][:pcp][:token_file] = data['pcp']['token-file']
170
+ self[:transports][:pcp][:"token-file"] = data['pcp']['token-file']
137
171
  end
138
172
  if data['pcp']['task-environment']
139
- self[:transports][:pcp][:orch_task_environment] = data['pcp']['task-environment']
173
+ self[:transports][:pcp][:"task-environment"] = data['pcp']['task-environment']
174
+ end
175
+ end
176
+
177
+ if data['local']
178
+ if data['local']['tmpdir']
179
+ self[:transports][:local][:tmpdir] = data['local']['tmpdir']
140
180
  end
141
181
  end
142
182
  end
183
+ private :update_from_file
143
184
 
144
185
  def load_file(path)
145
186
  data = Bolt::Util.read_config_file(path, default_paths, 'config')
@@ -152,14 +193,14 @@ module Bolt
152
193
  end
153
194
 
154
195
  if options[:debug]
155
- self[:log_level] = :debug
196
+ self[:log]['console'][:level] = :debug
156
197
  elsif options[:verbose]
157
- self[:log_level] = :info
198
+ self[:log]['console'][:level] = :info
158
199
  end
159
200
 
160
201
  TRANSPORT_OPTIONS.each do |key|
161
202
  TRANSPORTS.each do |transport|
162
- unless %i[ssl host_key_check].any? { |k| k == key }
203
+ unless %i[ssl host_key_check task-environment].any? { |k| k == key }
163
204
  self[:transports][transport][key] = options[key] if options[key]
164
205
  next
165
206
  end
@@ -176,6 +217,27 @@ module Bolt
176
217
  end
177
218
  end
178
219
 
220
+ def update_from_inventory(data)
221
+ update_from_file(data)
222
+
223
+ if data['transport']
224
+ self[:transport] = data['transport']
225
+ end
226
+
227
+ # Add options that aren't allowed in a config file, but are allowed in inventory
228
+ %w[user password port].each do |opt|
229
+ (TRANSPORTS - [:pcp]).each do |transport|
230
+ if data[transport.to_s] && data[transport.to_s][opt]
231
+ self[:transports][transport][opt.to_sym] = data[transport.to_s][opt]
232
+ end
233
+ end
234
+ end
235
+
236
+ if data['ssh'] && data['ssh']['sudo-password']
237
+ self[:transports][:ssh][:sudo_password] = data['ssh']['sudo-password']
238
+ end
239
+ end
240
+
179
241
  def transport_conf
180
242
  { transport: self[:transport],
181
243
  transports: self[:transports] }
@@ -186,6 +248,16 @@ module Bolt
186
248
  self[:transports][transport]
187
249
  end
188
250
 
251
+ self[:log].each_pair do |name, params|
252
+ if params.key?(:level) && !Bolt::Logger.valid_level?(params[:level])
253
+ raise Bolt::CLIError,
254
+ "level of log #{name} must be one of: #{Bolt::Logger.levels.join(', ')}; received #{params[:level]}"
255
+ end
256
+ if params.key?(:append) && params[:append] != true && params[:append] != false
257
+ raise Bolt::CLIError, "append flag of log #{name} must be a Boolean, received #{params[:append]}"
258
+ end
259
+ end
260
+
189
261
  unless %w[human json].include? self[:format]
190
262
  raise Bolt::CLIError, "Unsupported format: '#{self[:format]}'"
191
263
  end
@@ -195,6 +267,16 @@ module Bolt
195
267
  "user to escalate to with --run-as")
196
268
  end
197
269
 
270
+ host_key = self[:transports][:ssh][:host_key_check]
271
+ unless !!host_key == host_key
272
+ raise Bolt::CLIError, 'host-key-check option must be a Boolean true or false'
273
+ end
274
+
275
+ ssl_flag = self[:transports][:winrm][:ssl]
276
+ unless !!ssl_flag == ssl_flag
277
+ raise Bolt::CLIError, 'ssl option must be a Boolean true or false'
278
+ end
279
+
198
280
  self[:transports].each_value do |v|
199
281
  timeout_value = v[:connect_timeout]
200
282
  unless timeout_value.is_a?(Integer) || timeout_value.nil?
@@ -10,6 +10,7 @@ require 'bolt/result_set'
10
10
  require 'bolt/transport/ssh'
11
11
  require 'bolt/transport/winrm'
12
12
  require 'bolt/transport/orch'
13
+ require 'bolt/transport/local'
13
14
 
14
15
  module Bolt
15
16
  class Executor
@@ -23,7 +24,8 @@ module Bolt
23
24
  @transports = {
24
25
  'ssh' => Concurrent::Delay.new { Bolt::Transport::SSH.new(config[:transports][:ssh] || {}) },
25
26
  'winrm' => Concurrent::Delay.new { Bolt::Transport::WinRM.new(config[:transports][:winrm] || {}) },
26
- 'pcp' => Concurrent::Delay.new { Bolt::Transport::Orch.new(config[:transports][:pcp] || {}) }
27
+ 'pcp' => Concurrent::Delay.new { Bolt::Transport::Orch.new(config[:transports][:pcp] || {}) },
28
+ 'local' => Concurrent::Delay.new { Bolt::Transport::Local.new(config[:transports][:local] || {}) }
27
29
  }
28
30
 
29
31
  # If a specific elevated log level has been requested, honor that.
@@ -39,7 +41,10 @@ module Bolt
39
41
  end
40
42
 
41
43
  def transport(transport)
42
- @transports[transport || 'ssh'].value
44
+ impl = @transports[transport || 'ssh']
45
+ # If there was an error creating the transport, ensure it gets thrown
46
+ impl.no_error!
47
+ impl.value
43
48
  end
44
49
 
45
50
  def summary(action, object, result)
@@ -58,10 +63,12 @@ module Bolt
58
63
  # transport, is yielded to the block in turn and the results all collected
59
64
  # into a single ResultSet.
60
65
  def batch_execute(targets)
61
- promises = targets.group_by(&:protocol).flat_map do |protocol, _protocol_targets|
66
+ promises = targets.group_by(&:protocol).flat_map do |protocol, protocol_targets|
62
67
  transport = transport(protocol)
63
- transport.batches(targets).flat_map do |batch|
64
- batch_promises = Hash[Array(batch).map { |target| [target, Concurrent::Promise.new(executor: :immediate)] }]
68
+ transport.batches(protocol_targets).flat_map do |batch|
69
+ batch_promises = Array(batch).each_with_object({}) do |target, h|
70
+ h[target] = Concurrent::Promise.new(executor: :immediate)
71
+ end
65
72
  # Pass this argument through to avoid retaining a reference to a
66
73
  # local variable that will change on the next iteration of the loop.
67
74
  @pool.post(batch_promises) do |result_promises|
@@ -42,6 +42,7 @@ module Bolt
42
42
  inventory = new(data, config)
43
43
  inventory.validate
44
44
  inventory.collect_groups
45
+ inventory.add_localhost
45
46
  inventory
46
47
  end
47
48
 
@@ -52,6 +53,7 @@ module Bolt
52
53
  @data = data ||= {}
53
54
  @groups = Group.new(data.merge('name' => 'all'))
54
55
  @group_lookup = {}
56
+ @target_vars = {}
55
57
  end
56
58
 
57
59
  def validate
@@ -63,6 +65,16 @@ module Bolt
63
65
  @group_lookup = @groups.collect_groups
64
66
  end
65
67
 
68
+ def add_localhost
69
+ # Append a 'localhost' group if not already present.
70
+ unless @group_lookup.include?('localhost') || @groups.node_names.include?('localhost')
71
+ @groups.nodes['localhost'] = {
72
+ 'name' => 'localhost',
73
+ 'config' => { 'transport' => 'local' }
74
+ }
75
+ end
76
+ end
77
+
66
78
  def get_targets(targets)
67
79
  targets = expand_targets(targets)
68
80
  targets = if targets.is_a? Array
@@ -73,12 +85,13 @@ module Bolt
73
85
  targets.map { |t| update_target(t) }
74
86
  end
75
87
 
76
- # Should this be a public method?
77
- def config_for(node_name)
78
- data = @groups.data_for(node_name)
79
- if data
80
- Bolt::Util.symbolize_keys(data['config'])
81
- end
88
+ def set_var(target, key, value)
89
+ data = { key => value }
90
+ set_vars_from_hash(target.name, data)
91
+ end
92
+
93
+ def vars(target)
94
+ @target_vars[target.name]
82
95
  end
83
96
 
84
97
  #### PRIVATE ####
@@ -87,18 +100,31 @@ module Bolt
87
100
  def groups_in(node_name)
88
101
  @groups.data_for(node_name)['groups'] || {}
89
102
  end
103
+ private :groups_in
90
104
 
91
105
  # Pass a target to get_targets for a public version of this
92
106
  # Should this reconfigure configured targets?
93
107
  def update_target(target)
94
- inv_conf = config_for(target.name)
95
- unless inv_conf
108
+ data = @groups.data_for(target.name) || {}
109
+
110
+ unless data['config']
96
111
  @logger.debug("Did not find #{target.name} in inventory")
97
- inv_conf = {}
112
+ data['config'] = {}
98
113
  end
99
114
 
100
- conf = Bolt::Util.deep_merge(@config.transport_conf, inv_conf)
101
- target.update_conf(conf)
115
+ unless data['vars']
116
+ @logger.debug("Did not find any variables for #{target.name} in inventory")
117
+ data['vars'] = {}
118
+ end
119
+
120
+ set_vars_from_hash(target.name, data['vars'])
121
+
122
+ # Use Config object to ensure config section is treated consistently with config file
123
+ conf = @config.deep_clone
124
+ conf.update_from_inventory(data['config'])
125
+ conf.validate
126
+
127
+ target.update_conf(conf.transport_conf)
102
128
  end
103
129
  private :update_target
104
130
 
@@ -142,5 +168,16 @@ module Bolt
142
168
  end
143
169
  end
144
170
  private :expand_targets
171
+
172
+ def set_vars_from_hash(target_name, data)
173
+ if data
174
+ # Instantiate empty vars hash in case no vars are defined
175
+ @target_vars[target_name] = @target_vars[target_name] || {}
176
+ # Assign target new merged vars hash
177
+ # This is essentially a copy-on-write to maintain the immutability of @target_vars
178
+ @target_vars[target_name] = @target_vars[target_name].merge(data).freeze
179
+ end
180
+ end
181
+ private :set_vars_from_hash
145
182
  end
146
183
  end
@@ -8,8 +8,8 @@ module Bolt
8
8
  def initialize(data)
9
9
  @logger = Logging.logger[self]
10
10
  @name = data['name']
11
-
12
11
  @nodes = {}
12
+
13
13
  if data['nodes']
14
14
  data['nodes'].each do |n|
15
15
  n = { 'name' => n } if n.is_a? String
@@ -21,6 +21,7 @@ module Bolt
21
21
  end
22
22
  end
23
23
 
24
+ @vars = data['vars'] || {}
24
25
  @config = data['config'] || {}
25
26
  @groups = if data['groups']
26
27
  data['groups'].map { |g| Group.new(g) }
@@ -32,6 +33,14 @@ module Bolt
32
33
  @rest = data.reject { |k, _| %w[name nodes config groups].include? k }
33
34
  end
34
35
 
36
+ def check_deprecated_config(context, name, config)
37
+ if config && config['transports']
38
+ msg = "#{context} #{name} contains invalid config option 'transports', see " \
39
+ "https://puppet.com/docs/bolt/0.x/inventory_file.html for the updated format"
40
+ raise ValidationError.new(msg, @name)
41
+ end
42
+ end
43
+
35
44
  def validate(used_names = Set.new, node_names = Set.new, depth = 0)
36
45
  raise ValidationError.new("Group does not have a name", nil) unless @name
37
46
  if used_names.include?(@name)
@@ -44,6 +53,8 @@ module Bolt
44
53
  end
45
54
  raise ValidationError.new("Group #{@name} is too deeply nested", @name) if depth > 1
46
55
 
56
+ check_deprecated_config('Group', @name, @config)
57
+
47
58
  used_names << @name
48
59
 
49
60
  @nodes.each_value do |n|
@@ -60,6 +71,8 @@ module Bolt
60
71
  raise ValidationError.new("Group #{n['name']} conflicts with node of the same name", n['name'])
61
72
  end
62
73
 
74
+ check_deprecated_config('Node', n['name'], n['config'])
75
+
63
76
  node_names << n['name']
64
77
  end
65
78
 
@@ -76,7 +89,7 @@ module Bolt
76
89
  end
77
90
 
78
91
  # The data functions below expect and return nil or a hash of the schema
79
- # { 'config' => Hash , groups => Array }
92
+ # { 'config' => Hash , 'vars' => Hash, groups => Array }
80
93
  # As we add more options beyond config this schema will grow
81
94
  def data_for(node_name)
82
95
  data_merge(group_collect(node_name), node_collect(node_name))
@@ -85,6 +98,7 @@ module Bolt
85
98
  def node_data(node_name)
86
99
  if (data = @nodes[node_name])
87
100
  { 'config' => data['config'] || {},
101
+ 'vars' => data['vars'] || {},
88
102
  # groups come from group_data
89
103
  'groups' => [] }
90
104
  end
@@ -92,11 +106,13 @@ module Bolt
92
106
 
93
107
  def group_data
94
108
  { 'config' => @config,
109
+ 'vars' => @vars,
95
110
  'groups' => [@name] }
96
111
  end
97
112
 
98
113
  def empty_data
99
114
  { 'config' => {},
115
+ 'vars' => {},
100
116
  'groups' => [] }
101
117
  end
102
118
 
@@ -107,6 +123,10 @@ module Bolt
107
123
 
108
124
  {
109
125
  'config' => Bolt::Util.deep_merge(data1['config'], data2['config']),
126
+ # Shallow merge instead of deep merge so that vars with a hash value
127
+ # are assigned a new hash, rather than merging the existing value
128
+ # with the value meant to replace it
129
+ 'vars' => data2['vars'].merge(data1['vars']),
110
130
  'groups' => data2['groups'] + data1['groups']
111
131
  }
112
132
  end