bolt 1.8.1 → 1.9.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: 65bfdfb39627ab98263d43ab9fd6389eaeca02f809091a85d169da3e56b7cdf4
4
- data.tar.gz: 2184a841229576fe544824d8bf1bd687e1c319411651f29beffab00a5bd0e971
3
+ metadata.gz: 5818403d3dac52071b37dc23c99dc29a74c790493ea192b1f1855ca93cda721a
4
+ data.tar.gz: '019a929cfd4a7bfd2ce2bd51cbf4fa4e48cea6bb577213f68ae33dabaa704aa4'
5
5
  SHA512:
6
- metadata.gz: af1ccedbc0ba492c0b3acc69f0b250c0dc48e32ee63f8ce6e9bfa8ef259e4dd2bdbe659a0d0e3a3f80ea4a731c394cc7c7582833a93ad7f0af07e05e0d1a0535
7
- data.tar.gz: 65c4688066c5f2af97e22941820cb5c3cc7805a40f103a36d97c43c833f101f93957d3576aa1126046c64be67d06ebf4e2b59f7960f9678ce47985e6c00693dc
6
+ metadata.gz: 352f34c5cdef73ccde6a84df1fbf722a562b251801a9d9d49c7b221237de7a03758ba57139559fb07f8124ce3f19d4723b7313725bcdde0fa88f9cb1eeaf64e6
7
+ data.tar.gz: 35a9b64a4c97465b4f72e0ad7e7b98e2fdf657b08b89653a024e46d5ef78fa0fd44f5e9db66f97f1f9b1fed399d29a277461e9b8e63c9601307b9811f138a823
@@ -63,18 +63,19 @@ Puppet::Functions.create_function(:apply_prep) do
63
63
  need_install, installed = versions.partition { |r| r['version'].nil? }
64
64
  installed.each do |r|
65
65
  Puppet.debug "Puppet Agent #{r['version']} installed on #{r.target.name}"
66
+ inventory.set_feature(r.target, 'puppet-agent')
66
67
  end
67
68
 
68
69
  unless need_install.empty?
69
70
  need_install_targets = need_install.map(&:target)
70
71
  run_task(executor, need_install_targets, 'puppet_agent::install')
71
-
72
+ # Service task works best when targets have puppet-agent feature
73
+ need_install_targets.each { |target| inventory.set_feature(target, 'puppet-agent') }
72
74
  # Ensure the Puppet service is stopped after new install
73
75
  run_task(executor, need_install_targets, 'service', 'action' => 'stop', 'name' => 'puppet')
74
76
  run_task(executor, need_install_targets, 'service', 'action' => 'disable', 'name' => 'puppet')
75
77
  end
76
78
  end
77
- targets.each { |target| inventory.set_feature(target, 'puppet-agent') }
78
79
 
79
80
  # Gather facts, including custom facts
80
81
  plugins = applicator.build_plugin_tarball do |mod|
@@ -3,7 +3,7 @@
3
3
  # Repeat the block until it returns a truthy value. Returns the value.
4
4
  Puppet::Functions.create_function(:'ctrl::do_until') do
5
5
  # @example Run a task until it succeeds
6
- # ctrl::do_until || {
6
+ # ctrl::do_until() || {
7
7
  # run_task('test', $target, _catch_errors => true).ok?
8
8
  # }
9
9
  dispatch :do_until do
@@ -7,6 +7,7 @@ require 'json'
7
7
  require 'logging'
8
8
  require 'minitar'
9
9
  require 'open3'
10
+ require 'bolt/error'
10
11
  require 'bolt/task'
11
12
  require 'bolt/apply_result'
12
13
  require 'bolt/util/puppet_log_level'
@@ -125,7 +125,7 @@ module Bolt
125
125
  # Require nodes to be parseable as a Target.
126
126
  begin
127
127
  Target.new(n)
128
- rescue Addressable::URI::InvalidURIError => e
128
+ rescue Bolt::ParseError => e
129
129
  @logger.debug(e)
130
130
  raise ValidationError.new("Invalid node name #{n}", @name)
131
131
  end
@@ -10,4 +10,6 @@ module Bolt
10
10
  super(msg, "bolt/puppetdb-error")
11
11
  end
12
12
  end
13
+
14
+ class PuppetDBFailoverError < PuppetDBError; end
13
15
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'json'
4
+ require 'logging'
4
5
  require 'uri'
5
6
  require 'httpclient'
6
7
 
@@ -11,6 +12,9 @@ module Bolt
11
12
 
12
13
  def initialize(config)
13
14
  @config = config
15
+ @bad_urls = []
16
+ @current_url = nil
17
+ @logger = Logging.logger[self]
14
18
  end
15
19
 
16
20
  def query_certnames(query)
@@ -41,22 +45,35 @@ module Bolt
41
45
 
42
46
  def make_query(query, path = nil)
43
47
  body = JSON.generate(query: query)
44
- url = "#{@config.uri}/pdb/query/v4"
48
+ url = "#{uri}/pdb/query/v4"
45
49
  url += "/#{path}" if path
46
50
 
47
51
  begin
48
52
  response = http_client.post(url, body: body, header: headers)
53
+ rescue SocketError, OpenSSL::SSL::SSLError, SystemCallError, Net::ProtocolError, IOError => err
54
+ raise Bolt::PuppetDBFailoverError, "Failed to query PuppetDB: #{err}"
49
55
  rescue StandardError => err
50
56
  raise Bolt::PuppetDBError, "Failed to query PuppetDB: #{err}"
51
57
  end
58
+
52
59
  if response.code != 200
53
- raise Bolt::PuppetDBError, "Failed to query PuppetDB: #{response.body}"
60
+ msg = "Failed to query PuppetDB: #{response.body}"
61
+ if response.code == 400
62
+ raise Bolt::PuppetDBError, msg
63
+ else
64
+ raise Bolt::PuppetDBFailoverError, msg
65
+ end
54
66
  end
67
+
55
68
  begin
56
69
  JSON.parse(response.body)
57
70
  rescue JSON::ParserError
58
71
  raise Bolt::PuppetDBError, "Unable to parse response as JSON: #{response.body}"
59
72
  end
73
+ rescue Bolt::PuppetDBFailoverError => err
74
+ @logger.error("Request to puppetdb at #{@current_url} failed with #{err}.")
75
+ reject_url
76
+ make_query(query, path)
60
77
  end
61
78
 
62
79
  def http_client
@@ -68,6 +85,23 @@ module Bolt
68
85
  @http
69
86
  end
70
87
 
88
+ def reject_url
89
+ @bad_urls << @current_url if @current_url
90
+ @current_url = nil
91
+ end
92
+
93
+ def uri
94
+ @current_url ||= (@config.server_urls - @bad_urls).first
95
+ unless @current_url
96
+ msg = "Failed to connect to all PuppetDB server_urls: #{@config.server_urls.to_a.join(', ')}."
97
+ raise Bolt::PuppetDBError, msg
98
+ end
99
+
100
+ uri = URI.parse(@current_url)
101
+ uri.port ||= 8081
102
+ uri
103
+ end
104
+
71
105
  def headers
72
106
  headers = { 'Content-Type' => 'application/json' }
73
107
  headers['X-Authentication'] = @config.token if @config.token
@@ -20,6 +20,7 @@ module Bolt
20
20
  end
21
21
 
22
22
  def self.load_config(filename, options)
23
+ config = {}
23
24
  global_path = Bolt::Util.windows? ? DEFAULT_CONFIG[:win_global] : DEFAULT_CONFIG[:global]
24
25
  if filename
25
26
  if File.exist?(filename)
@@ -27,13 +28,20 @@ module Bolt
27
28
  else
28
29
  raise Bolt::PuppetDBError, "config file #{filename} does not exist"
29
30
  end
30
- elsif File.exist?(DEFAULT_CONFIG[:user])
31
- config = JSON.parse(File.read(DEFAULT_CONFIG[:user]))
32
- elsif File.exist?(global_path)
33
- config = JSON.parse(File.read(global_path))
34
31
  else
35
- config = {}
32
+ if File.exist?(DEFAULT_CONFIG[:user])
33
+ filepath = DEFAULT_CONFIG[:user]
34
+ elsif File.exist?(global_path)
35
+ filepath = global_path
36
+ end
37
+
38
+ begin
39
+ config = JSON.parse(File.read(filepath)) if filepath
40
+ rescue StandardError => e
41
+ Logging.logger[self].error("Could not load puppetdb.conf from #{filepath}: #{e.message}")
42
+ end
36
43
  end
44
+
37
45
  config = config.fetch('puppetdb', {})
38
46
  new(config.merge(options))
39
47
  end
@@ -45,8 +53,11 @@ module Bolt
45
53
 
46
54
  def token
47
55
  return @token if @token
48
- if @settings['token']
49
- @token = File.read(@settings['token'])
56
+ # Allow nil in config to skip loading a token
57
+ if @settings.include?('token')
58
+ if @settings['token']
59
+ @token = File.read(@settings['token'])
60
+ end
50
61
  elsif File.exist?(DEFAULT_TOKEN)
51
62
  @token = File.read(DEFAULT_TOKEN)
52
63
  end
@@ -66,6 +77,19 @@ module Bolt
66
77
  true
67
78
  end
68
79
 
80
+ def server_urls
81
+ case @settings['server_urls']
82
+ when String
83
+ [@settings['server_urls']]
84
+ when Array
85
+ @settings['server_urls']
86
+ when nil
87
+ raise Bolt::PuppetDBError, "server_urls must be specified"
88
+ else
89
+ raise Bolt::PuppetDBError, "server_urls must be a string or array"
90
+ end
91
+ end
92
+
69
93
  def uri
70
94
  return @uri if @uri
71
95
  uri = case @settings['server_urls']
@@ -62,6 +62,8 @@ module Bolt
62
62
  # Initialize with an empty scheme to ensure we parse the hostname correctly
63
63
  Addressable::URI.parse("//#{string}")
64
64
  end
65
+ rescue Addressable::URI::InvalidURIError => e
66
+ raise Bolt::ParseError, "Could not parse target URI: #{e.message}"
65
67
  end
66
68
  private :parse
67
69
 
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bolt/transport/api/connection'
4
+
5
+ # A copy of the orchestrator transport which uses the api connection class
6
+ # in order to bypass calling 'start_plan'
7
+ module Bolt
8
+ module Transport
9
+ class Api < Orch
10
+ def initialize(*args)
11
+ super
12
+ end
13
+
14
+ def get_connection(conn_opts)
15
+ key = Bolt::Transport::Api::Connection.get_key(conn_opts)
16
+ unless (conn = @connections[key])
17
+ @connections[key] = Bolt::Transport::Api::Connection.new(conn_opts, logger)
18
+ conn = @connections[key]
19
+ end
20
+ conn
21
+ end
22
+
23
+ def batches(targets)
24
+ targets.group_by { |target| Bolt::Transport::Api::Connection.get_key(target.options) }.values
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This is a copy of the orchestrator transport connection, but without the
4
+ # 'start_plan' call in the init function which is handled by the orchestrator
5
+ # client
6
+ module Bolt
7
+ module Transport
8
+ class Api < Orch
9
+ class Connection
10
+ attr_reader :logger, :key
11
+
12
+ CONTEXT_KEYS = Set.new(%i[plan_name description params]).freeze
13
+
14
+ def self.get_key(opts)
15
+ [
16
+ opts['service-url'],
17
+ opts['task-environment'],
18
+ opts['token-file']
19
+ ].join('-')
20
+ end
21
+
22
+ def initialize(opts, logger)
23
+ @logger = logger
24
+ @key = self.class.get_key(opts)
25
+ client_keys = %w[service-url token-file cacert]
26
+ client_opts = client_keys.each_with_object({}) do |k, acc|
27
+ acc[k] = opts[k] if opts.include?(k)
28
+ end
29
+ client_opts['User-Agent'] = "Bolt/#{VERSION}"
30
+ logger.debug("Creating orchestrator client for #{client_opts}")
31
+
32
+ @client = OrchestratorClient.new(client_opts, true)
33
+ @environment = opts["task-environment"]
34
+ end
35
+
36
+ def finish_plan(plan_result)
37
+ if @plan_job
38
+ @client.command.plan_finish(
39
+ plan_job: @plan_job,
40
+ result: plan_result.value || '',
41
+ status: plan_result.status
42
+ )
43
+ end
44
+ end
45
+
46
+ def build_request(targets, task, arguments, description = nil)
47
+ body = { task: task.name,
48
+ environment: @environment,
49
+ noop: arguments['_noop'],
50
+ params: arguments.reject { |k, _| k.start_with?('_') },
51
+ scope: {
52
+ nodes: targets.map(&:host)
53
+ } }
54
+ body[:description] = description if description
55
+ body[:plan_job] = @plan_job if @plan_job
56
+ body
57
+ end
58
+
59
+ def run_task(targets, task, arguments, options)
60
+ body = build_request(targets, task, arguments, options['_description'])
61
+ @client.run_task(body)
62
+ end
63
+
64
+ def query_inventory(targets)
65
+ @client.post('inventory', nodes: targets.map(&:host))
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -36,11 +36,15 @@ module Bolt
36
36
 
37
37
  def in_tmpdir(base)
38
38
  args = base ? [nil, base] : []
39
- Dir.mktmpdir(*args) do |dir|
40
- yield dir
41
- end
42
- rescue StandardError => e
43
- raise Bolt::Node::FileError.new("Could not make tempdir: #{e.message}", 'TEMPDIR_ERROR')
39
+ dir = begin
40
+ Dir.mktmpdir(*args)
41
+ rescue StandardError => e
42
+ raise Bolt::Node::FileError.new("Could not make tempdir: #{e.message}", 'TEMPDIR_ERROR')
43
+ end
44
+
45
+ yield dir
46
+ ensure
47
+ FileUtils.remove_entry dir if dir
44
48
  end
45
49
  private :in_tmpdir
46
50
 
@@ -19,7 +19,7 @@ module Bolt
19
19
  result_output = Bolt::Node::Output.new
20
20
  result_output.stdout << stdout unless stdout.nil?
21
21
  result_output.stderr << stderr unless stderr.nil?
22
- result_output.exit_code = rc.to_i
22
+ result_output.exit_code = rc.exitstatus
23
23
  result_output
24
24
  end
25
25
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Bolt
4
- VERSION = '1.8.1'
4
+ VERSION = '1.9.0'
5
5
  end
@@ -8,6 +8,7 @@ require 'bolt/inventory'
8
8
  require 'bolt/pal'
9
9
  require 'bolt/puppetdb'
10
10
  require 'plan_executor/applicator'
11
+ require 'plan_executor/executor'
11
12
  require 'concurrent'
12
13
  require 'json'
13
14
  require 'json-schema'
@@ -31,7 +32,7 @@ module PlanExecutor
31
32
  @worker = Concurrent::SingleThreadExecutor.new
32
33
 
33
34
  # Create a basic executor, leave concurrency up to Orchestrator.
34
- @executor = executor || Bolt::Executor.new(0, load_config: false)
35
+ @executor = executor || PlanExecutor::Executor.new(0)
35
36
  # Use an empty inventory until we figure out where this data comes from.
36
37
  @inventory = Bolt::Inventory.new(nil)
37
38
  # TODO: what should max compiles be set to for apply?
@@ -0,0 +1,194 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Used for $ERROR_INFO. This *must* be capitalized!
4
+ require 'English'
5
+ require 'json'
6
+ require 'logging'
7
+ require 'set'
8
+ require 'bolt/result'
9
+ require 'bolt/config'
10
+ require 'bolt/transport/api'
11
+ require 'bolt/notifier'
12
+ require 'bolt/result_set'
13
+ require 'bolt/puppetdb'
14
+
15
+ module PlanExecutor
16
+ class Executor
17
+ attr_reader :noop, :transport
18
+
19
+ def initialize(noop = nil)
20
+ @logger = Logging.logger[self]
21
+ @plan_logging = false
22
+ @noop = noop
23
+ @logger.debug { "Started" }
24
+ @notifier = Bolt::Notifier.new
25
+ @transport = Bolt::Transport::Api.new
26
+ end
27
+
28
+ # This handles running the job, catching errors, and turning the result
29
+ # into a result set
30
+ def execute(targets)
31
+ result_array = begin
32
+ yield
33
+ rescue StandardError => e
34
+ @logger.warn(e)
35
+ # CODEREVIEW how should we fail if there's an error?
36
+ Array(Bolt::Result.from_exception(targets[0], e))
37
+ end
38
+ Bolt::ResultSet.new(result_array)
39
+ end
40
+
41
+ # TODO: Remove in favor of service logging
42
+ def log_action(description, targets)
43
+ # When running a plan, info messages like starting a task are promoted to notice.
44
+ log_method = @plan_logging ? :notice : :info
45
+ target_str = if targets.length > 5
46
+ "#{targets.count} targets"
47
+ else
48
+ targets.map(&:uri).join(', ')
49
+ end
50
+
51
+ @logger.send(log_method, "Starting: #{description} on #{target_str}")
52
+
53
+ start_time = Time.now
54
+ results = yield
55
+ duration = Time.now - start_time
56
+
57
+ failures = results.error_set.length
58
+ plural = failures == 1 ? '' : 's'
59
+
60
+ @logger.send(log_method, "Finished: #{description} with #{failures} failure#{plural} in #{duration.round(2)} sec")
61
+
62
+ results
63
+ end
64
+
65
+ def log_plan(plan_name)
66
+ log_method = @plan_logging ? :notice : :info
67
+ @logger.send(log_method, "Starting: plan #{plan_name}")
68
+ start_time = Time.now
69
+
70
+ results = nil
71
+ begin
72
+ results = yield
73
+ ensure
74
+ duration = Time.now - start_time
75
+ @logger.send(log_method, "Finished: plan #{plan_name} in #{duration.round(2)} sec")
76
+ end
77
+
78
+ results
79
+ end
80
+
81
+ def run_command(targets, command, options = {}, &callback)
82
+ description = options.fetch('_description', "command '#{command}'")
83
+ log_action(description, targets) do
84
+ notify = proc { |event| @notifier.notify(callback, event) if callback }
85
+
86
+ results = execute(targets) do
87
+ @transport.batch_command(targets, command, options, &notify)
88
+ end
89
+
90
+ @notifier.shutdown
91
+ results
92
+ end
93
+ end
94
+
95
+ def run_script(targets, script, arguments, options = {}, &callback)
96
+ description = options.fetch('_description', "script #{script}")
97
+ log_action(description, targets) do
98
+ notify = proc { |event| @notifier.notify(callback, event) if callback }
99
+
100
+ results = execute(targets) do
101
+ @transport.batch_script(targets, script, arguments, options, &notify)
102
+ end
103
+
104
+ @notifier.shutdown
105
+ results
106
+ end
107
+ end
108
+
109
+ def run_task(targets, task, arguments, options = {}, &callback)
110
+ description = options.fetch('_description', "task #{task.name}")
111
+ log_action(description, targets) do
112
+ notify = proc { |event| @notifier.notify(callback, event) if callback }
113
+
114
+ arguments['_task'] = task.name
115
+
116
+ results = execute(targets) do
117
+ @transport.batch_task(targets, task, arguments, options, &notify)
118
+ end
119
+
120
+ @notifier.shutdown
121
+ results
122
+ end
123
+ end
124
+
125
+ def upload_file(targets, source, destination, options = {}, &callback)
126
+ description = options.fetch('_description', "file upload from #{source} to #{destination}")
127
+ log_action(description, targets) do
128
+ notify = proc { |event| @notifier.notify(callback, event) if callback }
129
+
130
+ results = execute(targets) do
131
+ @transport.batch_upload(targets, source, destination, options, &notify)
132
+ end
133
+
134
+ @notifier.shutdown
135
+ results
136
+ end
137
+ end
138
+
139
+ class TimeoutError < RuntimeError; end
140
+
141
+ def wait_until_available(targets,
142
+ description: 'wait until available',
143
+ wait_time: 120,
144
+ retry_interval: 1)
145
+ log_action(description, targets) do
146
+ begin
147
+ wait_until(wait_time, retry_interval) { @transport.batch_connected?(targets) }
148
+ targets.map { |target| Bolt::Result.new(target) }
149
+ rescue TimeoutError => e
150
+ targets.map { |target| Bolt::Result.from_exception(target, e) }
151
+ end
152
+ end
153
+ end
154
+
155
+ def wait_until(timeout, retry_interval)
156
+ start = wait_now
157
+ until yield
158
+ raise(TimeoutError, 'Timed out waiting for target') if (wait_now - start).to_i >= timeout
159
+ sleep(retry_interval)
160
+ end
161
+ end
162
+
163
+ # Plan context doesn't make sense for most transports but it is tightly
164
+ # coupled with the orchestrator transport since the transport behaves
165
+ # differently when a plan is running. In order to limit how much this
166
+ # pollutes the transport API we only handle the orchestrator transport here.
167
+ # Since we callt this function without resolving targets this will result
168
+ # in the orchestrator transport always being initialized during plan runs.
169
+ # For now that's ok.
170
+ #
171
+ # In the future if other transports need this or if we want a plan stack
172
+ # we'll need to refactor.
173
+ def start_plan(plan_context)
174
+ @transport.plan_context = plan_context
175
+ @plan_logging = true
176
+ end
177
+
178
+ def finish_plan(plan_result)
179
+ @transport.finish_plan(plan_result)
180
+ end
181
+
182
+ def without_default_logging
183
+ old_log = @plan_logging
184
+ @plan_logging = false
185
+ yield
186
+ ensure
187
+ @plan_logging = old_log
188
+ end
189
+
190
+ def report_bundled_content(mode, name); end
191
+
192
+ def report_function_call(function); end
193
+ end
194
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bolt
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.8.1
4
+ version: 1.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Puppet
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2019-01-04 00:00:00.000000000 Z
11
+ date: 2019-01-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: addressable
@@ -357,6 +357,8 @@ files:
357
357
  - lib/bolt/task.rb
358
358
  - lib/bolt/task/puppet_server.rb
359
359
  - lib/bolt/task/remote.rb
360
+ - lib/bolt/transport/api.rb
361
+ - lib/bolt/transport/api/connection.rb
360
362
  - lib/bolt/transport/base.rb
361
363
  - lib/bolt/transport/docker.rb
362
364
  - lib/bolt/transport/docker/connection.rb
@@ -393,6 +395,7 @@ files:
393
395
  - lib/plan_executor/app.rb
394
396
  - lib/plan_executor/applicator.rb
395
397
  - lib/plan_executor/config.rb
398
+ - lib/plan_executor/executor.rb
396
399
  - lib/plan_executor/schemas/run_plan.json
397
400
  - libexec/apply_catalog.rb
398
401
  - libexec/bolt_catalog