bolt 0.16.1 → 0.16.2
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/lib/bolt.rb +0 -1
- data/lib/bolt/cli.rb +12 -6
- data/lib/bolt/error.rb +8 -0
- data/lib/bolt/executor.rb +99 -113
- data/lib/bolt/inventory.rb +47 -2
- data/lib/bolt/inventory/group.rb +37 -6
- data/lib/bolt/pal.rb +28 -9
- data/lib/bolt/target.rb +0 -8
- data/lib/bolt/transport/base.rb +159 -0
- data/lib/bolt/transport/orch.rb +158 -0
- data/lib/bolt/transport/ssh.rb +135 -0
- data/lib/bolt/transport/ssh/connection.rb +278 -0
- data/lib/bolt/transport/winrm.rb +165 -0
- data/lib/bolt/transport/winrm/connection.rb +472 -0
- data/lib/bolt/version.rb +1 -1
- data/modules/boltlib/lib/puppet/datatypes/target.rb +5 -0
- data/modules/boltlib/lib/puppet/functions/fail_plan.rb +1 -1
- data/modules/boltlib/lib/puppet/functions/file_upload.rb +1 -5
- data/modules/boltlib/lib/puppet/functions/get_targets.rb +41 -0
- data/modules/boltlib/lib/puppet/functions/run_command.rb +2 -6
- data/modules/boltlib/lib/puppet/functions/run_plan.rb +4 -1
- data/modules/boltlib/lib/puppet/functions/run_script.rb +2 -6
- data/modules/boltlib/lib/puppet/functions/run_task.rb +15 -17
- data/modules/boltlib/types/targetspec.pp +7 -0
- metadata +10 -6
- data/lib/bolt/node.rb +0 -76
- data/lib/bolt/node/orch.rb +0 -126
- data/lib/bolt/node/ssh.rb +0 -356
- data/lib/bolt/node/winrm.rb +0 -598
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ee1bb84782f71c0faea509d91746ce03e9749432
|
4
|
+
data.tar.gz: 636871a24286b9ca256ffd8c3e31c3224a20ea97
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: bede0c32f199f41154a2223f74e83c9c7e62a6ea32edadd0850db0bc6faa655603126750e949b1ee2a9192186c527ba3a848205688568bbfcf1bdcb7eab45129
|
7
|
+
data.tar.gz: b008ffcdd129e7b785a0d93cdb4c37b128d3ab28be930fd7f9e8e5fa40ac5b4250c094be60a2ba12117806c9194029298b6d89df638cc6e945585d9b9b523e0d
|
data/lib/bolt.rb
CHANGED
data/lib/bolt/cli.rb
CHANGED
@@ -9,7 +9,6 @@ require 'bolt/error'
|
|
9
9
|
require 'bolt/executor'
|
10
10
|
require 'bolt/inventory'
|
11
11
|
require 'bolt/logger'
|
12
|
-
require 'bolt/node'
|
13
12
|
require 'bolt/outputter'
|
14
13
|
require 'bolt/pal'
|
15
14
|
require 'bolt/target'
|
@@ -19,7 +18,6 @@ module Bolt
|
|
19
18
|
class CLIError < Bolt::Error
|
20
19
|
def initialize(msg)
|
21
20
|
super(msg, "bolt/cli-error")
|
22
|
-
@error_code = error_code if error_code
|
23
21
|
end
|
24
22
|
end
|
25
23
|
|
@@ -136,7 +134,7 @@ HELP
|
|
136
134
|
'* protocol is `ssh` by default, may be `ssh` or `winrm`',
|
137
135
|
'* port defaults to `22` for SSH',
|
138
136
|
'* port defaults to `5985` or `5986` for WinRM, based on the --[no-]ssl setting') do |nodes|
|
139
|
-
results[:nodes] << get_arg_input(nodes)
|
137
|
+
results[:nodes] << get_arg_input(nodes)
|
140
138
|
end
|
141
139
|
end
|
142
140
|
opts.on('-u', '--user USER',
|
@@ -259,6 +257,12 @@ HELP
|
|
259
257
|
parser
|
260
258
|
end
|
261
259
|
|
260
|
+
# Only call after @config has been initialized.
|
261
|
+
def inventory
|
262
|
+
Bolt::Inventory.from_config(@config)
|
263
|
+
end
|
264
|
+
private :inventory
|
265
|
+
|
262
266
|
def parse
|
263
267
|
if @argv.empty?
|
264
268
|
options[:help] = true
|
@@ -309,8 +313,11 @@ HELP
|
|
309
313
|
|
310
314
|
validate(options)
|
311
315
|
|
316
|
+
# After validation, initialize inventory and targets. Errors here are better to catch early.
|
317
|
+
options[:targets] = inventory.get_targets(options[:nodes]) if options[:nodes]
|
318
|
+
|
312
319
|
options
|
313
|
-
rescue Bolt::
|
320
|
+
rescue Bolt::Error => e
|
314
321
|
warn e.message
|
315
322
|
raise e
|
316
323
|
end
|
@@ -434,7 +441,6 @@ HELP
|
|
434
441
|
return 0
|
435
442
|
end
|
436
443
|
|
437
|
-
inventory = Bolt::Inventory.from_config(@config)
|
438
444
|
message = 'There may be processes left executing on some nodes.'
|
439
445
|
|
440
446
|
if options[:mode] == 'plan'
|
@@ -445,7 +451,7 @@ HELP
|
|
445
451
|
code = 0
|
446
452
|
else
|
447
453
|
executor = Bolt::Executor.new(@config, options[:noop])
|
448
|
-
targets =
|
454
|
+
targets = options[:targets]
|
449
455
|
|
450
456
|
results = nil
|
451
457
|
outputter.print_head
|
data/lib/bolt/error.rb
CHANGED
@@ -29,6 +29,14 @@ module Bolt
|
|
29
29
|
def to_puppet_error
|
30
30
|
Puppet::DataTypes::Error.from_asserted_hash(to_h)
|
31
31
|
end
|
32
|
+
|
33
|
+
def self.unknown_task(task)
|
34
|
+
"Could not find a task named \"#{task}\". For a list of available tasks, run \"bolt task show\""
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.unknown_plan(plan)
|
38
|
+
"Could not find a plan named \"#{plan}\". For a list of available plans, run \"bolt plan show\""
|
39
|
+
end
|
32
40
|
end
|
33
41
|
|
34
42
|
class RunFailure < Error
|
data/lib/bolt/executor.rb
CHANGED
@@ -1,21 +1,31 @@
|
|
1
|
+
# Used for $ERROR_INFO. This *must* be capitalized!
|
2
|
+
require 'English'
|
1
3
|
require 'json'
|
2
4
|
require 'concurrent'
|
3
5
|
require 'logging'
|
4
6
|
require 'bolt/result'
|
5
7
|
require 'bolt/config'
|
6
8
|
require 'bolt/notifier'
|
7
|
-
require 'bolt/node'
|
8
9
|
require 'bolt/result_set'
|
10
|
+
require 'bolt/transport/ssh'
|
11
|
+
require 'bolt/transport/winrm'
|
12
|
+
require 'bolt/transport/orch'
|
9
13
|
|
10
14
|
module Bolt
|
11
15
|
class Executor
|
12
|
-
attr_reader :noop
|
16
|
+
attr_reader :noop, :transports
|
13
17
|
attr_accessor :run_as
|
14
18
|
|
15
19
|
def initialize(config = Bolt::Config.new, noop = nil, plan_logging = false)
|
16
20
|
@config = config
|
17
21
|
@logger = Logging.logger[self]
|
18
22
|
|
23
|
+
@transports = {
|
24
|
+
'ssh' => Concurrent::Delay.new { Bolt::Transport::SSH.new(config[:transports][:ssh] || {}) },
|
25
|
+
'winrm' => Concurrent::Delay.new { Bolt::Transport::WinRM.new(config[:transports][:winrm] || {}) },
|
26
|
+
'pcp' => Concurrent::Delay.new { Bolt::Transport::Orch.new(config[:transports][:pcp] || {}) }
|
27
|
+
}
|
28
|
+
|
19
29
|
# If a specific elevated log level has been requested, honor that.
|
20
30
|
# Otherwise, escalate the log level to "info" if running in plan mode, so
|
21
31
|
# that certain progress messages will be visible.
|
@@ -23,55 +33,14 @@ module Bolt
|
|
23
33
|
@logger.level = @config[:log_level] || default_log_level
|
24
34
|
@noop = noop
|
25
35
|
@run_as = nil
|
36
|
+
@pool = Concurrent::CachedThreadPool.new(max_threads: @config[:concurrency])
|
37
|
+
@logger.debug { "Started with #{@config[:concurrency]} max thread(s)" }
|
26
38
|
@notifier = Bolt::Notifier.new
|
27
39
|
end
|
28
40
|
|
29
|
-
def
|
30
|
-
|
31
|
-
Bolt::Node.from_target(target)
|
32
|
-
end
|
33
|
-
end
|
34
|
-
private :from_targets
|
35
|
-
|
36
|
-
def on(nodes, callback = nil)
|
37
|
-
results = Concurrent::Array.new
|
38
|
-
|
39
|
-
poolsize = [nodes.length, @config[:concurrency]].min
|
40
|
-
pool = Concurrent::FixedThreadPool.new(poolsize)
|
41
|
-
@logger.debug { "Started with #{poolsize} thread(s)" }
|
42
|
-
|
43
|
-
nodes.map(&:class).uniq.each do |klass|
|
44
|
-
klass.initialize_transport(@logger)
|
45
|
-
end
|
46
|
-
|
47
|
-
nodes.each { |node|
|
48
|
-
pool.post do
|
49
|
-
result =
|
50
|
-
begin
|
51
|
-
@notifier.notify(callback, type: :node_start, target: node.target) if callback
|
52
|
-
node.connect
|
53
|
-
yield node
|
54
|
-
rescue StandardError => ex
|
55
|
-
Bolt::Result.from_exception(node.target, ex)
|
56
|
-
ensure
|
57
|
-
begin
|
58
|
-
node.disconnect
|
59
|
-
rescue StandardError => ex
|
60
|
-
@logger.info("Failed to close connection to #{node.uri} : #{ex.message}")
|
61
|
-
end
|
62
|
-
end
|
63
|
-
results.concat([result])
|
64
|
-
@notifier.notify(callback, type: :node_result, result: result) if callback
|
65
|
-
end
|
66
|
-
}
|
67
|
-
pool.shutdown
|
68
|
-
pool.wait_for_termination
|
69
|
-
|
70
|
-
@notifier.shutdown
|
71
|
-
|
72
|
-
Bolt::ResultSet.new(results)
|
41
|
+
def transport(transport)
|
42
|
+
@transports[transport || 'ssh'].value
|
73
43
|
end
|
74
|
-
private :on
|
75
44
|
|
76
45
|
def summary(action, object, result)
|
77
46
|
fc = result.error_set.length
|
@@ -81,90 +50,107 @@ module Bolt
|
|
81
50
|
end
|
82
51
|
private :summary
|
83
52
|
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
53
|
+
# Execute the given block on a list of nodes in parallel, one thread per "batch".
|
54
|
+
#
|
55
|
+
# This is the main driver of execution on a list of targets. It first
|
56
|
+
# groups targets by transport, then divides each group into batches as
|
57
|
+
# defined by the transport. Each batch, along with the corresponding
|
58
|
+
# transport, is yielded to the block in turn and the results all collected
|
59
|
+
# into a single ResultSet.
|
60
|
+
def batch_execute(targets)
|
61
|
+
promises = targets.group_by(&:protocol).flat_map do |protocol, _protocol_targets|
|
62
|
+
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)] }]
|
65
|
+
# Pass this argument through to avoid retaining a reference to a
|
66
|
+
# local variable that will change on the next iteration of the loop.
|
67
|
+
@pool.post(batch_promises) do |result_promises|
|
68
|
+
begin
|
69
|
+
results = yield transport, batch
|
70
|
+
Array(results).each do |result|
|
71
|
+
result_promises[result.target].set(result)
|
72
|
+
end
|
73
|
+
# NotImplementedError can be thrown if the transport is implemented improperly
|
74
|
+
rescue StandardError, NotImplementedError => e
|
75
|
+
result_promises.each do |target, promise|
|
76
|
+
promise.set(Bolt::Result.from_exception(target, e))
|
77
|
+
end
|
78
|
+
ensure
|
79
|
+
# Make absolutely sure every promise gets a result to avoid a
|
80
|
+
# deadlock. Use whatever exception is causing this block to
|
81
|
+
# execute, or generate one if we somehow got here without an
|
82
|
+
# exception and some promise is still missing a result.
|
83
|
+
result_promises.each do |target, promise|
|
84
|
+
next if promise.fulfilled?
|
85
|
+
error = $ERROR_INFO || Bolt::Error.new("No result was returned for #{target.uri}",
|
86
|
+
"puppetlabs.bolt/missing-result-error")
|
87
|
+
promise.set(Bolt::Result.from_exception(error))
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
batch_promises.values
|
92
|
+
end
|
89
93
|
end
|
94
|
+
ResultSet.new(promises.map(&:value))
|
90
95
|
end
|
91
|
-
private :get_run_as
|
92
96
|
|
93
|
-
def
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
end
|
98
|
-
private :with_exception_handling
|
99
|
-
|
100
|
-
def run_command(targets, command, options = {})
|
101
|
-
nodes = from_targets(targets)
|
102
|
-
@logger.info("Starting command run '#{command}' on #{nodes.map(&:uri)}")
|
103
|
-
callback = block_given? ? Proc.new : nil
|
97
|
+
def run_command(targets, command, options = {}, &callback)
|
98
|
+
@logger.info("Starting command run '#{command}' on #{targets.map(&:uri)}")
|
99
|
+
notify = proc { |event| @notifier.notify(callback, event) if callback }
|
100
|
+
options = { '_run_as' => run_as }.merge(options) if run_as
|
104
101
|
|
105
|
-
|
106
|
-
|
107
|
-
node_result = with_exception_handling(node) do
|
108
|
-
node.run_command(command, get_run_as(node, options))
|
109
|
-
end
|
110
|
-
@logger.debug("Result on #{node.uri}: #{JSON.dump(node_result.value)}")
|
111
|
-
node_result
|
102
|
+
results = batch_execute(targets) do |transport, batch|
|
103
|
+
transport.batch_command(batch, command, options, ¬ify)
|
112
104
|
end
|
113
|
-
|
114
|
-
|
105
|
+
|
106
|
+
@logger.info(summary('command', command, results))
|
107
|
+
@notifier.shutdown
|
108
|
+
results
|
115
109
|
end
|
116
110
|
|
117
|
-
def run_script(targets, script, arguments, options = {})
|
118
|
-
|
119
|
-
@logger.info("Starting script run #{script} on #{nodes.map(&:uri)}")
|
111
|
+
def run_script(targets, script, arguments, options = {}, &callback)
|
112
|
+
@logger.info("Starting script run #{script} on #{targets.map(&:uri)}")
|
120
113
|
@logger.debug("Arguments: #{arguments}")
|
121
|
-
|
114
|
+
notify = proc { |event| @notifier.notify(callback, event) if callback }
|
115
|
+
options = { '_run_as' => run_as }.merge(options) if run_as
|
122
116
|
|
123
|
-
|
124
|
-
|
125
|
-
node_result = with_exception_handling(node) do
|
126
|
-
node.run_script(script, arguments, get_run_as(node, options))
|
127
|
-
end
|
128
|
-
@logger.debug("Result on #{node.uri}: #{JSON.dump(node_result.value)}")
|
129
|
-
node_result
|
117
|
+
results = batch_execute(targets) do |transport, batch|
|
118
|
+
transport.batch_script(batch, script, arguments, options, ¬ify)
|
130
119
|
end
|
131
|
-
|
132
|
-
|
120
|
+
|
121
|
+
@logger.info(summary('script', script, results))
|
122
|
+
@notifier.shutdown
|
123
|
+
results
|
133
124
|
end
|
134
125
|
|
135
|
-
def run_task(targets, task,
|
136
|
-
|
137
|
-
@logger.info("Starting task #{
|
138
|
-
@logger.debug("Arguments: #{arguments} Input method: #{input_method}")
|
139
|
-
|
126
|
+
def run_task(targets, task, arguments, options = {}, &callback)
|
127
|
+
task_name = task.name
|
128
|
+
@logger.info("Starting task #{task_name} on #{targets.map(&:uri)}")
|
129
|
+
@logger.debug("Arguments: #{arguments} Input method: #{task.input_method}")
|
130
|
+
notify = proc { |event| @notifier.notify(callback, event) if callback }
|
131
|
+
options = { '_run_as' => run_as }.merge(options) if run_as
|
140
132
|
|
141
|
-
|
142
|
-
|
143
|
-
node_result = with_exception_handling(node) do
|
144
|
-
node.run_task(task, input_method, arguments, get_run_as(node, options))
|
145
|
-
end
|
146
|
-
@logger.debug("Result on #{node.uri}: #{JSON.dump(node_result.value)}")
|
147
|
-
node_result
|
133
|
+
results = batch_execute(targets) do |transport, batch|
|
134
|
+
transport.batch_task(batch, task, arguments, options, ¬ify)
|
148
135
|
end
|
149
|
-
|
150
|
-
|
136
|
+
|
137
|
+
@logger.info(summary('task', task_name, results))
|
138
|
+
@notifier.shutdown
|
139
|
+
results
|
151
140
|
end
|
152
141
|
|
153
|
-
def file_upload(targets, source, destination, options = {})
|
154
|
-
|
155
|
-
|
156
|
-
|
142
|
+
def file_upload(targets, source, destination, options = {}, &callback)
|
143
|
+
@logger.info("Starting file upload from #{source} to #{destination} on #{targets.map(&:uri)}")
|
144
|
+
notify = proc { |event| @notifier.notify(callback, event) if callback }
|
145
|
+
options = { '_run_as' => run_as }.merge(options) if run_as
|
157
146
|
|
158
|
-
|
159
|
-
|
160
|
-
node_result = with_exception_handling(node) do
|
161
|
-
node.upload(source, destination, options)
|
162
|
-
end
|
163
|
-
@logger.debug("Result on #{node.uri}: #{JSON.dump(node_result.value)}")
|
164
|
-
node_result
|
147
|
+
results = batch_execute(targets) do |transport, batch|
|
148
|
+
transport.batch_upload(batch, source, destination, options, ¬ify)
|
165
149
|
end
|
166
|
-
|
167
|
-
|
150
|
+
|
151
|
+
@logger.info(summary('upload', source, results))
|
152
|
+
@notifier.shutdown
|
153
|
+
results
|
168
154
|
end
|
169
155
|
end
|
170
156
|
end
|
data/lib/bolt/inventory.rb
CHANGED
@@ -26,6 +26,12 @@ module Bolt
|
|
26
26
|
end
|
27
27
|
end
|
28
28
|
|
29
|
+
class WildcardError < Bolt::Error
|
30
|
+
def initialize(target)
|
31
|
+
super("Found 0 nodes matching wildcard pattern #{target}", 'bolt.inventory/wildcard-error')
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
29
35
|
def self.default_paths
|
30
36
|
[File.expand_path(File.join('~', '.puppetlabs', 'bolt', 'inventory.yaml'))]
|
31
37
|
end
|
@@ -35,21 +41,28 @@ module Bolt
|
|
35
41
|
|
36
42
|
inventory = new(data, config)
|
37
43
|
inventory.validate
|
44
|
+
inventory.collect_groups
|
38
45
|
inventory
|
39
46
|
end
|
40
47
|
|
41
48
|
def initialize(data, config = nil)
|
42
49
|
@logger = Logging.logger[self]
|
43
50
|
# Config is saved to add config options to targets
|
44
|
-
@config = config ||
|
51
|
+
@config = config || Bolt::Config.new
|
45
52
|
@data = data ||= {}
|
46
53
|
@groups = Group.new(data.merge('name' => 'all'))
|
54
|
+
@group_lookup = {}
|
47
55
|
end
|
48
56
|
|
49
57
|
def validate
|
50
58
|
@groups.validate
|
51
59
|
end
|
52
60
|
|
61
|
+
def collect_groups
|
62
|
+
# Provide a lookup map for finding a group by name
|
63
|
+
@group_lookup = @groups.collect_groups
|
64
|
+
end
|
65
|
+
|
53
66
|
def get_targets(targets)
|
54
67
|
targets = expand_targets(targets)
|
55
68
|
targets = if targets.is_a? Array
|
@@ -87,6 +100,33 @@ module Bolt
|
|
87
100
|
conf = Bolt::Util.deep_merge(@config.transport_conf, inv_conf)
|
88
101
|
target.update_conf(conf)
|
89
102
|
end
|
103
|
+
private :update_target
|
104
|
+
|
105
|
+
# If target is a group name, expand it to the members of that group.
|
106
|
+
# If a wildcard string, match against nodes in inventory (or error if none found).
|
107
|
+
# Else return [target].
|
108
|
+
def resolve_name(target)
|
109
|
+
if (group = @group_lookup[target])
|
110
|
+
group.node_names
|
111
|
+
elsif target.include?('*')
|
112
|
+
# Try to wildcard match nodes in inventory
|
113
|
+
# Ignore case because hostnames are generally case-insensitive
|
114
|
+
regexp = Regexp.new("^#{Regexp.escape(target).gsub('\*', '.*?')}$", Regexp::IGNORECASE)
|
115
|
+
|
116
|
+
nodes = []
|
117
|
+
@groups.node_names.each do |node|
|
118
|
+
if node =~ regexp
|
119
|
+
nodes << node
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
raise(WildcardError, target) if nodes.empty?
|
124
|
+
nodes
|
125
|
+
else
|
126
|
+
[target]
|
127
|
+
end
|
128
|
+
end
|
129
|
+
private :resolve_name
|
90
130
|
|
91
131
|
def expand_targets(targets)
|
92
132
|
if targets.is_a? Bolt::Target
|
@@ -94,8 +134,13 @@ module Bolt
|
|
94
134
|
elsif targets.is_a? Array
|
95
135
|
targets.map { |tish| expand_targets(tish) }
|
96
136
|
elsif targets.is_a? String
|
97
|
-
|
137
|
+
# Expand a comma-separated list
|
138
|
+
targets.split(/[[:space:],]+/).reject(&:empty?).map do |name|
|
139
|
+
ts = resolve_name(name)
|
140
|
+
ts.map { |t| Bolt::Target.new(t) }
|
141
|
+
end
|
98
142
|
end
|
99
143
|
end
|
144
|
+
private :expand_targets
|
100
145
|
end
|
101
146
|
end
|