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.
- checksums.yaml +4 -4
- data/bolt-modules/boltlib/lib/puppet/functions/fail_plan.rb +2 -3
- data/bolt-modules/boltlib/lib/puppet/functions/file_upload.rb +2 -2
- data/bolt-modules/boltlib/lib/puppet/functions/get_targets.rb +2 -3
- data/bolt-modules/boltlib/lib/puppet/functions/run_command.rb +2 -2
- data/bolt-modules/boltlib/lib/puppet/functions/run_plan.rb +2 -2
- data/bolt-modules/boltlib/lib/puppet/functions/run_task.rb +2 -2
- data/bolt-modules/boltlib/lib/puppet/functions/set_var.rb +29 -0
- data/bolt-modules/boltlib/lib/puppet/functions/vars.rb +27 -0
- data/lib/bolt/cli.rb +220 -184
- data/lib/bolt/config.rb +95 -13
- data/lib/bolt/executor.rb +12 -5
- data/lib/bolt/inventory.rb +48 -11
- data/lib/bolt/inventory/group.rb +22 -2
- data/lib/bolt/logger.rb +56 -8
- data/lib/bolt/outputter/human.rb +18 -1
- data/lib/bolt/pal.rb +6 -1
- data/lib/bolt/target.rb +1 -1
- data/lib/bolt/transport/base.rb +3 -0
- data/lib/bolt/transport/local.rb +90 -0
- data/lib/bolt/transport/local/shell.rb +29 -0
- data/lib/bolt/transport/orch.rb +2 -2
- data/lib/bolt/transport/ssh.rb +0 -3
- data/lib/bolt/transport/winrm.rb +0 -3
- data/lib/bolt/util.rb +31 -4
- data/lib/bolt/version.rb +1 -1
- data/lib/bolt_ext/puppetdb_inventory.rb +9 -2
- data/modules/aggregate/lib/puppet/functions/aggregate/count.rb +19 -0
- data/modules/aggregate/lib/puppet/functions/aggregate/nodes.rb +19 -0
- data/modules/aggregate/plans/count.pp +35 -0
- data/modules/aggregate/plans/nodes.pp +35 -0
- data/modules/canary/lib/puppet/functions/canary/merge.rb +11 -0
- data/modules/canary/lib/puppet/functions/canary/random_split.rb +20 -0
- data/modules/canary/lib/puppet/functions/canary/skip.rb +23 -0
- data/modules/canary/plans/init.pp +52 -0
- metadata +14 -16
data/lib/bolt/config.rb
CHANGED
@@ -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
|
-
|
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']
|
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][:
|
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][:
|
170
|
+
self[:transports][:pcp][:"token-file"] = data['pcp']['token-file']
|
137
171
|
end
|
138
172
|
if data['pcp']['task-environment']
|
139
|
-
self[:transports][:pcp][:
|
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[:
|
196
|
+
self[:log]['console'][:level] = :debug
|
156
197
|
elsif options[:verbose]
|
157
|
-
self[:
|
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?
|
data/lib/bolt/executor.rb
CHANGED
@@ -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']
|
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,
|
66
|
+
promises = targets.group_by(&:protocol).flat_map do |protocol, protocol_targets|
|
62
67
|
transport = transport(protocol)
|
63
|
-
transport.batches(
|
64
|
-
batch_promises =
|
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|
|
data/lib/bolt/inventory.rb
CHANGED
@@ -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
|
-
|
77
|
-
|
78
|
-
data
|
79
|
-
|
80
|
-
|
81
|
-
|
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
|
-
|
95
|
-
|
108
|
+
data = @groups.data_for(target.name) || {}
|
109
|
+
|
110
|
+
unless data['config']
|
96
111
|
@logger.debug("Did not find #{target.name} in inventory")
|
97
|
-
|
112
|
+
data['config'] = {}
|
98
113
|
end
|
99
114
|
|
100
|
-
|
101
|
-
|
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
|
data/lib/bolt/inventory/group.rb
CHANGED
@@ -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
|