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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 4c145190f4fb70b998c8fd501a368940c2d2db3c
4
- data.tar.gz: 45949da83f3903d369a71f88730e8ef7413cdff7
3
+ metadata.gz: ee1bb84782f71c0faea509d91746ce03e9749432
4
+ data.tar.gz: 636871a24286b9ca256ffd8c3e31c3224a20ea97
5
5
  SHA512:
6
- metadata.gz: 4597004b2108d16ab6aaf401e0b5160ef711ce27fe487b1996457f3b007ffc522c438b6df3a4b540b7e7566b9bc3794ecee4474400dbf21393b132b5abf99134
7
- data.tar.gz: fe62dba570d0e11ed31e2be2e490a8eaa347485edf9adca4798e6c731ae892f48ad767f6f3493f1f54f42df7468b091c9a8db057e6bc65b2037eb607817b3152
6
+ metadata.gz: bede0c32f199f41154a2223f74e83c9c7e62a6ea32edadd0850db0bc6faa655603126750e949b1ee2a9192186c527ba3a848205688568bbfcf1bdcb7eab45129
7
+ data.tar.gz: b008ffcdd129e7b785a0d93cdb4c37b128d3ab28be930fd7f9e8e5fa40ac5b4250c094be60a2ba12117806c9194029298b6d89df638cc6e945585d9b9b523e0d
@@ -1,4 +1,3 @@
1
1
  module Bolt
2
2
  require 'bolt/executor'
3
- require 'bolt/node'
4
3
  end
@@ -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).split(/[[:space:],]+/).reject(&:empty?)
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::CLIError => e
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 = inventory.get_targets(options[:nodes])
454
+ targets = options[:targets]
449
455
 
450
456
  results = nil
451
457
  outputter.print_head
@@ -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
@@ -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 from_targets(targets)
30
- targets.map do |target|
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
- def get_run_as(node, options)
85
- if node.run_as.nil? && run_as
86
- { '_run_as' => run_as }.merge(options)
87
- else
88
- options
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 with_exception_handling(node)
94
- yield
95
- rescue StandardError => e
96
- Bolt::Result.from_exception(node.target, e)
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
- r = on(nodes, callback) do |node|
106
- @logger.debug("Running command '#{command}' on #{node.uri}")
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, &notify)
112
104
  end
113
- @logger.info(summary('command', command, r))
114
- r
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
- nodes = from_targets(targets)
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
- callback = block_given? ? Proc.new : nil
114
+ notify = proc { |event| @notifier.notify(callback, event) if callback }
115
+ options = { '_run_as' => run_as }.merge(options) if run_as
122
116
 
123
- r = on(nodes, callback) do |node|
124
- @logger.debug { "Running script '#{script}' on #{node.uri}" }
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, &notify)
130
119
  end
131
- @logger.info(summary('script', script, r))
132
- r
120
+
121
+ @logger.info(summary('script', script, results))
122
+ @notifier.shutdown
123
+ results
133
124
  end
134
125
 
135
- def run_task(targets, task, input_method, arguments, options = {})
136
- nodes = from_targets(targets)
137
- @logger.info("Starting task #{task} on #{nodes.map(&:uri)}")
138
- @logger.debug("Arguments: #{arguments} Input method: #{input_method}")
139
- callback = block_given? ? Proc.new : nil
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
- r = on(nodes, callback) do |node|
142
- @logger.debug { "Running task run '#{task}' on #{node.uri}" }
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, &notify)
148
135
  end
149
- @logger.info(summary('task', task, r))
150
- r
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
- nodes = from_targets(targets)
155
- @logger.info("Starting file upload from #{source} to #{destination} on #{nodes.map(&:uri)}")
156
- callback = block_given? ? Proc.new : nil
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
- r = on(nodes, callback) do |node|
159
- @logger.debug { "Uploading: '#{source}' to #{destination} on #{node.uri}" }
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, &notify)
165
149
  end
166
- @logger.info(summary('upload', source, r))
167
- r
150
+
151
+ @logger.info(summary('upload', source, results))
152
+ @notifier.shutdown
153
+ results
168
154
  end
169
155
  end
170
156
  end
@@ -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
- Bolt::Target.new(targets)
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