bolt 1.43.0 → 1.44.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/file/lib/puppet/functions/file/join.rb +17 -0
- data/lib/bolt/config.rb +1 -1
- data/lib/bolt/util.rb +14 -3
- data/lib/bolt/version.rb +1 -1
- data/lib/bolt_server/transport_app.rb +5 -0
- metadata +3 -8
- data/lib/plan_executor/app.rb +0 -141
- data/lib/plan_executor/applicator.rb +0 -30
- data/lib/plan_executor/config.rb +0 -43
- data/lib/plan_executor/executor.rb +0 -167
- data/lib/plan_executor/orch_client.rb +0 -241
- data/lib/plan_executor/schemas/run_plan.json +0 -29
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3e7033e924c3721ad51ea3ba221527831174bf0a962a09be2d01af56eea9d29a
|
4
|
+
data.tar.gz: 400125a4441d5df92ac45fae11e222be1b0d8925ea501144e61f82f4a60fc62a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b25b113a4b89a14c84aec41fe2e8e1fad3a41a82ac4b6e58e8ff3901e46ac2be2a71eafee0cc80545f9d03cdb6d93affd1e44dc90f426fdb415bbfd26e17d16d
|
7
|
+
data.tar.gz: 6b08cf2a56959c286622c1279b2583ef312c673cb08c719727ba4c29142f8e9ea2d449f12a811eba671a7658b1619a735203905d98bae6dfc66ecb7c00b958a0
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Join file paths.
|
4
|
+
Puppet::Functions.create_function(:'file::join') do
|
5
|
+
# @param paths The paths to join.
|
6
|
+
# @example Join file paths
|
7
|
+
# file::join('./path', 'to/files')
|
8
|
+
dispatch :join do
|
9
|
+
required_repeated_param 'String', :paths
|
10
|
+
return_type 'String'
|
11
|
+
end
|
12
|
+
|
13
|
+
def join(*paths)
|
14
|
+
Puppet.lookup(:bolt_executor) {}&.report_function_call(self.class.name)
|
15
|
+
File.join(paths)
|
16
|
+
end
|
17
|
+
end
|
data/lib/bolt/config.rb
CHANGED
@@ -223,7 +223,7 @@ module Bolt
|
|
223
223
|
if @future
|
224
224
|
to_expand = %w[private-key cacert token-file] & selected.keys
|
225
225
|
to_expand.each do |opt|
|
226
|
-
selected[opt] = File.expand_path(selected[opt], @boltdir.path) if opt.is_a?(String)
|
226
|
+
selected[opt] = File.expand_path(selected[opt], @boltdir.path) if selected[opt].is_a?(String)
|
227
227
|
end
|
228
228
|
end
|
229
229
|
|
data/lib/bolt/util.rb
CHANGED
@@ -169,12 +169,23 @@ module Bolt
|
|
169
169
|
# references to the same object and prevents endless recursion.
|
170
170
|
# Credit to Jan Molic via https://github.com/rubyworks/facets/blob/master/LICENSE.txt
|
171
171
|
def deep_clone(obj, cloned = {})
|
172
|
-
cloned[obj.object_id] if cloned.include?(obj.object_id)
|
172
|
+
return cloned[obj.object_id] if cloned.include?(obj.object_id)
|
173
|
+
|
174
|
+
# The `defined?` method will not reliably find the Java::JavaLang::CloneNotSupportedException constant
|
175
|
+
# presumably due to some sort of optimization that short-cuts doing a bunch of Java introspection.
|
176
|
+
# Java::JavaLang::<...> IS defining the constant (via const_missing or const_get magic perhaps) so
|
177
|
+
# it is safe to reference it in the error_types array when a JRuby interpreter is evaluating the code
|
178
|
+
# (detected by RUBY_PLATFORM == `java`). SO instead of conditionally adding the CloneNotSupportedException
|
179
|
+
# constant to the error_types array based on `defined?` detecting the Java::JavaLang constant it is added
|
180
|
+
# based on detecting a JRuby interpreter.
|
181
|
+
# TypeError handles unclonable Ruby ojbects (TrueClass, Fixnum, ...)
|
182
|
+
# CloneNotSupportedException handles uncloneable Java objects (JRuby only)
|
183
|
+
error_types = [TypeError]
|
184
|
+
error_types << Java::JavaLang::CloneNotSupportedException if RUBY_PLATFORM == 'java'
|
173
185
|
|
174
186
|
begin
|
175
187
|
cl = obj.clone
|
176
|
-
rescue
|
177
|
-
# unclonable (TrueClass, Fixnum, ...)
|
188
|
+
rescue *error_types
|
178
189
|
cloned[obj.object_id] = obj
|
179
190
|
obj
|
180
191
|
else
|
data/lib/bolt/version.rb
CHANGED
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.
|
4
|
+
version: 1.44.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Puppet
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2020-01-09 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: addressable
|
@@ -367,6 +367,7 @@ files:
|
|
367
367
|
- bolt-modules/ctrl/lib/puppet/functions/ctrl/do_until.rb
|
368
368
|
- bolt-modules/ctrl/lib/puppet/functions/ctrl/sleep.rb
|
369
369
|
- bolt-modules/file/lib/puppet/functions/file/exists.rb
|
370
|
+
- bolt-modules/file/lib/puppet/functions/file/join.rb
|
370
371
|
- bolt-modules/file/lib/puppet/functions/file/read.rb
|
371
372
|
- bolt-modules/file/lib/puppet/functions/file/readable.rb
|
372
373
|
- bolt-modules/file/lib/puppet/functions/file/write.rb
|
@@ -485,12 +486,6 @@ files:
|
|
485
486
|
- lib/bolt_spec/plans/publish_stub.rb
|
486
487
|
- lib/bolt_spec/run.rb
|
487
488
|
- lib/logging_extensions/logging.rb
|
488
|
-
- lib/plan_executor/app.rb
|
489
|
-
- lib/plan_executor/applicator.rb
|
490
|
-
- lib/plan_executor/config.rb
|
491
|
-
- lib/plan_executor/executor.rb
|
492
|
-
- lib/plan_executor/orch_client.rb
|
493
|
-
- lib/plan_executor/schemas/run_plan.json
|
494
489
|
- libexec/apply_catalog.rb
|
495
490
|
- libexec/bolt_catalog
|
496
491
|
- libexec/custom_facts.rb
|
data/lib/plan_executor/app.rb
DELETED
@@ -1,141 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'sinatra'
|
4
|
-
require 'bolt'
|
5
|
-
require 'bolt/error'
|
6
|
-
require 'bolt/inventory'
|
7
|
-
require 'bolt/pal'
|
8
|
-
require 'bolt/puppetdb'
|
9
|
-
require 'bolt/version'
|
10
|
-
require 'plan_executor/applicator'
|
11
|
-
require 'plan_executor/executor'
|
12
|
-
require 'json'
|
13
|
-
require 'json-schema'
|
14
|
-
|
15
|
-
module PlanExecutor
|
16
|
-
class App < Sinatra::Base
|
17
|
-
# This disables Sinatra's error page generation
|
18
|
-
set :show_exceptions, false
|
19
|
-
# Global var to capture output for testing
|
20
|
-
result = nil
|
21
|
-
|
22
|
-
helpers do
|
23
|
-
def puppetdb_client
|
24
|
-
return @puppetdb_client if @puppetdb_client
|
25
|
-
@puppetdb_client = Bolt::PuppetDB::Client.new({})
|
26
|
-
end
|
27
|
-
end
|
28
|
-
|
29
|
-
def initialize(config)
|
30
|
-
# lazy-load expensive gem code
|
31
|
-
require 'concurrent'
|
32
|
-
|
33
|
-
@http_client = create_http(config)
|
34
|
-
|
35
|
-
# Use an empty inventory until we figure out where this data comes from.
|
36
|
-
@inventory = Bolt::Inventory.new(nil)
|
37
|
-
|
38
|
-
# PAL is not threadsafe. Part of the work of making the plan executor
|
39
|
-
# functional will be making changes to Puppet that remove the need for
|
40
|
-
# global Puppet state.
|
41
|
-
# https://github.com/puppetlabs/bolt/blob/master/lib/bolt/pal.rb#L166
|
42
|
-
@pal = Bolt::PAL.new(config['modulepath'], nil, nil)
|
43
|
-
|
44
|
-
@schema = JSON.parse(File.read(File.join(__dir__, 'schemas', 'run_plan.json')))
|
45
|
-
@worker = Concurrent::SingleThreadExecutor.new
|
46
|
-
@modulepath = config['modulepath']
|
47
|
-
|
48
|
-
super(nil)
|
49
|
-
end
|
50
|
-
|
51
|
-
def create_http(config)
|
52
|
-
base_url = config['orchestrator-url'].chomp('/') + '/orchestrator/v1/'
|
53
|
-
agent_name = "Bolt/#{Bolt::VERSION}"
|
54
|
-
http = JSONClient.new(base_url: base_url, agent_name: agent_name)
|
55
|
-
http.ssl_config.set_client_cert_file(config['ssl-cert'], config['ssl-key'])
|
56
|
-
http.ssl_config.add_trust_ca(config['ssl-ca-cert'])
|
57
|
-
http
|
58
|
-
end
|
59
|
-
|
60
|
-
def validate_schema(schema, body)
|
61
|
-
schema_error = JSON::Validator.fully_validate(schema, body)
|
62
|
-
if schema_error.any?
|
63
|
-
Bolt::Error.new("There was an error validating the request body.",
|
64
|
-
'boltserver/schema-error',
|
65
|
-
schema_error)
|
66
|
-
end
|
67
|
-
end
|
68
|
-
|
69
|
-
get '/' do
|
70
|
-
200
|
71
|
-
end
|
72
|
-
|
73
|
-
if ENV['RACK_ENV'] == 'dev'
|
74
|
-
get '/admin/gc' do
|
75
|
-
GC.start
|
76
|
-
200
|
77
|
-
end
|
78
|
-
|
79
|
-
get '/admin/gc_stat' do
|
80
|
-
[200, GC.stat.to_json]
|
81
|
-
end
|
82
|
-
end
|
83
|
-
|
84
|
-
get '/500_error' do
|
85
|
-
raise 'Unexpected error'
|
86
|
-
end
|
87
|
-
|
88
|
-
post '/plan/run' do
|
89
|
-
content_type :json
|
90
|
-
|
91
|
-
body = JSON.parse(request.body.read)
|
92
|
-
error = validate_schema(@schema, body)
|
93
|
-
return [400, error.to_json] unless error.nil?
|
94
|
-
name = body['plan_name']
|
95
|
-
# We need to wrap all calls to @pal (not just plan_run) in a future
|
96
|
-
# to ensure that the process always uses the SingleThreadExecutor
|
97
|
-
# worker and forces one call to @pal at a time regardless of the number
|
98
|
-
# of concurrent calls to POST /plan_run
|
99
|
-
result = Concurrent::Future.execute(executor: @worker) do
|
100
|
-
@pal.get_plan_info(name)
|
101
|
-
end
|
102
|
-
# .value! will fail if the internal process of the thread fails
|
103
|
-
result.value!
|
104
|
-
executor = PlanExecutor::Executor.new(body['job_id'], @http_client)
|
105
|
-
applicator = PlanExecutor::Applicator.new(@inventory, executor, nil)
|
106
|
-
params = body['params']
|
107
|
-
# This provides a wait function, which promise doesn't
|
108
|
-
result = Concurrent::Future.execute(executor: @worker) do
|
109
|
-
pal_result = @pal.run_plan(name, params, executor, @inventory, puppetdb_client, applicator)
|
110
|
-
executor.finish_plan(pal_result)
|
111
|
-
pal_result
|
112
|
-
end
|
113
|
-
[200, { status: 'running' }.to_json]
|
114
|
-
end
|
115
|
-
|
116
|
-
# Provided for testing
|
117
|
-
get '/plan/result' do
|
118
|
-
result.wait_or_cancel(20)
|
119
|
-
if result.fulfilled?
|
120
|
-
return [200, result.value.to_json]
|
121
|
-
elsif result.rejected?
|
122
|
-
raise result.reason.to_s
|
123
|
-
else
|
124
|
-
return [200, result.state.to_s]
|
125
|
-
end
|
126
|
-
end
|
127
|
-
|
128
|
-
error 404 do
|
129
|
-
err = Bolt::Error.new("Could not find route #{request.path}",
|
130
|
-
'boltserver/not-found')
|
131
|
-
[404, err.to_json]
|
132
|
-
end
|
133
|
-
|
134
|
-
error 500 do
|
135
|
-
e = env['sinatra.error']
|
136
|
-
err = Bolt::Error.new("500: Unknown error: #{e.message}",
|
137
|
-
'boltserver/server-error')
|
138
|
-
[500, err.to_json]
|
139
|
-
end
|
140
|
-
end
|
141
|
-
end
|
@@ -1,30 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'bolt/error'
|
4
|
-
|
5
|
-
module PlanExecutor
|
6
|
-
class Applicator
|
7
|
-
def initialize(inventory, executor, config)
|
8
|
-
@inventory = inventory
|
9
|
-
@executor = executor
|
10
|
-
@config = config
|
11
|
-
end
|
12
|
-
|
13
|
-
def raise_not_implemented(feature)
|
14
|
-
raise Bolt::Error.new("#{feature} not implemented for plan executor service.",
|
15
|
-
'bolt.plan-executor/not-implemented')
|
16
|
-
end
|
17
|
-
|
18
|
-
def apply(_args, _apply_body, _scope)
|
19
|
-
raise_not_implemented("apply")
|
20
|
-
end
|
21
|
-
|
22
|
-
def build_plugin_tarball
|
23
|
-
raise_not_implemented("build_plugin_tarball")
|
24
|
-
end
|
25
|
-
|
26
|
-
def custom_facts_task
|
27
|
-
raise_not_implemented('custom_facts_task')
|
28
|
-
end
|
29
|
-
end
|
30
|
-
end
|
data/lib/plan_executor/config.rb
DELETED
@@ -1,43 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'hocon'
|
4
|
-
require 'bolt_server/base_config'
|
5
|
-
require 'bolt/error'
|
6
|
-
|
7
|
-
module PlanExecutor
|
8
|
-
class Config < BoltServer::BaseConfig
|
9
|
-
def config_keys
|
10
|
-
super + %w[modulepath workers orchestrator-url]
|
11
|
-
end
|
12
|
-
|
13
|
-
def defaults
|
14
|
-
super.merge(
|
15
|
-
'port' => 62659,
|
16
|
-
'workers' => 1
|
17
|
-
)
|
18
|
-
end
|
19
|
-
|
20
|
-
def service_name
|
21
|
-
'plan-executor'
|
22
|
-
end
|
23
|
-
|
24
|
-
def required_keys
|
25
|
-
super + ['orchestrator-url']
|
26
|
-
end
|
27
|
-
|
28
|
-
def load_env_config
|
29
|
-
env_keys.each do |key|
|
30
|
-
transformed_key = "BOLT_#{key.tr('-', '_').upcase}"
|
31
|
-
next unless ENV.key?(transformed_key)
|
32
|
-
@data[key] = ENV[transformed_key]
|
33
|
-
end
|
34
|
-
end
|
35
|
-
|
36
|
-
def validate
|
37
|
-
super
|
38
|
-
unless natural?(@data['workers'])
|
39
|
-
raise Bolt::ValidationError, "Configured 'workers' must be a positive integer"
|
40
|
-
end
|
41
|
-
end
|
42
|
-
end
|
43
|
-
end
|
@@ -1,167 +0,0 @@
|
|
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/result_set'
|
11
|
-
require 'bolt/puppetdb'
|
12
|
-
require 'plan_executor/orch_client'
|
13
|
-
|
14
|
-
module PlanExecutor
|
15
|
-
class Executor
|
16
|
-
attr_reader :noop, :logger
|
17
|
-
attr_accessor :orch_client
|
18
|
-
|
19
|
-
def initialize(job_id, http_client, noop = nil)
|
20
|
-
@logger = Logging.logger[self]
|
21
|
-
@plan_logging = false
|
22
|
-
@noop = noop
|
23
|
-
@logger.debug { "Started" }
|
24
|
-
@orch_client = PlanExecutor::OrchClient.new(job_id, http_client, @logger)
|
25
|
-
end
|
26
|
-
|
27
|
-
# This handles running the job, catching errors, and turning the result
|
28
|
-
# into a result set
|
29
|
-
def as_resultset(targets)
|
30
|
-
result_array = begin
|
31
|
-
yield
|
32
|
-
rescue StandardError => e
|
33
|
-
@logger.warn(e)
|
34
|
-
# CODEREVIEW how should we fail if there's an error?
|
35
|
-
Array(Bolt::Result.from_exception(targets[0], e))
|
36
|
-
end
|
37
|
-
Bolt::ResultSet.new(result_array)
|
38
|
-
end
|
39
|
-
|
40
|
-
# BOLT-1098
|
41
|
-
def log_action(description, targets)
|
42
|
-
# When running a plan, info messages like starting a task are promoted to notice.
|
43
|
-
log_method = @plan_logging ? :notice : :info
|
44
|
-
target_str = if targets.length > 5
|
45
|
-
"#{targets.count} targets"
|
46
|
-
else
|
47
|
-
targets.map(&:uri).join(', ')
|
48
|
-
end
|
49
|
-
|
50
|
-
@logger.send(log_method, "Starting: #{description} on #{target_str}")
|
51
|
-
|
52
|
-
start_time = Time.now
|
53
|
-
results = yield
|
54
|
-
duration = Time.now - start_time
|
55
|
-
|
56
|
-
failures = results.error_set.length
|
57
|
-
plural = failures == 1 ? '' : 's'
|
58
|
-
|
59
|
-
@logger.send(log_method, "Finished: #{description} with #{failures} failure#{plural} in #{duration.round(2)} sec")
|
60
|
-
|
61
|
-
results
|
62
|
-
end
|
63
|
-
|
64
|
-
# BOLT-1098
|
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 = {})
|
82
|
-
description = options.fetch(:description, "command '#{command}'")
|
83
|
-
log_action(description, targets) do
|
84
|
-
results = as_resultset(targets) do
|
85
|
-
@orch_client.run_command(targets, command, options)
|
86
|
-
end
|
87
|
-
|
88
|
-
results
|
89
|
-
end
|
90
|
-
end
|
91
|
-
|
92
|
-
def run_script(targets, script, arguments, options = {})
|
93
|
-
description = options.fetch(:description, "script #{script}")
|
94
|
-
log_action(description, targets) do
|
95
|
-
results = as_resultset(targets) do
|
96
|
-
@orch_client.run_script(targets, script, arguments, options)
|
97
|
-
end
|
98
|
-
|
99
|
-
results
|
100
|
-
end
|
101
|
-
end
|
102
|
-
|
103
|
-
def run_task(targets, task, arguments, options = {})
|
104
|
-
description = options.fetch(:description, "task #{task.name}")
|
105
|
-
log_action(description, targets) do
|
106
|
-
arguments['_task'] = task.name
|
107
|
-
|
108
|
-
results = as_resultset(targets) do
|
109
|
-
@orch_client.run_task(targets, task, arguments, options)
|
110
|
-
end
|
111
|
-
|
112
|
-
results
|
113
|
-
end
|
114
|
-
end
|
115
|
-
|
116
|
-
def upload_file(targets, source, destination, options = {})
|
117
|
-
description = options.fetch(:description, "file upload from #{source} to #{destination}")
|
118
|
-
log_action(description, targets) do
|
119
|
-
results = as_resultset(targets) do
|
120
|
-
@orch_client.file_upload(targets, source, destination, options)
|
121
|
-
end
|
122
|
-
|
123
|
-
results
|
124
|
-
end
|
125
|
-
end
|
126
|
-
|
127
|
-
class TimeoutError < RuntimeError; end
|
128
|
-
|
129
|
-
def wait_until_available(targets,
|
130
|
-
description: 'wait until available',
|
131
|
-
wait_time: 120,
|
132
|
-
retry_interval: 1)
|
133
|
-
log_action(description, targets) do
|
134
|
-
begin
|
135
|
-
wait_until(wait_time, retry_interval) { @orch_client.connected?(targets) }
|
136
|
-
targets.map { |target| Bolt::Result.new(target) }
|
137
|
-
rescue TimeoutError => e
|
138
|
-
targets.map { |target| Bolt::Result.from_exception(target, e) }
|
139
|
-
end
|
140
|
-
end
|
141
|
-
end
|
142
|
-
|
143
|
-
def wait_until(timeout, retry_interval)
|
144
|
-
start = wait_now
|
145
|
-
until yield
|
146
|
-
raise(TimeoutError, 'Timed out waiting for target') if (wait_now - start).to_i >= timeout
|
147
|
-
sleep(retry_interval)
|
148
|
-
end
|
149
|
-
end
|
150
|
-
|
151
|
-
def finish_plan(plan_result)
|
152
|
-
@orch_client.finish_plan(plan_result)
|
153
|
-
end
|
154
|
-
|
155
|
-
def without_default_logging
|
156
|
-
old_log = @plan_logging
|
157
|
-
@plan_logging = false
|
158
|
-
yield
|
159
|
-
ensure
|
160
|
-
@plan_logging = old_log
|
161
|
-
end
|
162
|
-
|
163
|
-
def report_bundled_content(mode, name); end
|
164
|
-
|
165
|
-
def report_function_call(function); end
|
166
|
-
end
|
167
|
-
end
|
@@ -1,241 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'jsonclient'
|
4
|
-
|
5
|
-
module PlanExecutor
|
6
|
-
class OrchClient
|
7
|
-
attr_reader :plan_job, :http
|
8
|
-
|
9
|
-
BOLT_COMMAND_TASK = Struct.new(:name).new('bolt_shim::command').freeze
|
10
|
-
BOLT_SCRIPT_TASK = Struct.new(:name).new('bolt_shim::script').freeze
|
11
|
-
BOLT_UPLOAD_TASK = Struct.new(:name).new('bolt_shim::upload').freeze
|
12
|
-
|
13
|
-
def initialize(plan_job, http_client, logger)
|
14
|
-
@plan_job = plan_job
|
15
|
-
@logger = logger
|
16
|
-
@http = http_client
|
17
|
-
@environment = 'production'
|
18
|
-
end
|
19
|
-
|
20
|
-
def finish_plan(plan_result)
|
21
|
-
body = {
|
22
|
-
plan_job: @plan_job,
|
23
|
-
result: plan_result.value || '',
|
24
|
-
status: plan_result.status
|
25
|
-
}
|
26
|
-
post_command('internal/plan_finish', body)
|
27
|
-
rescue StandardError => e
|
28
|
-
@logger.error("Failed to finish plan #{plan_job}: #{e.message}")
|
29
|
-
end
|
30
|
-
|
31
|
-
def run_task_job(targets, task, arguments, options)
|
32
|
-
# unpack any Sensitive data
|
33
|
-
arguments = unwrap_sensitive_args(arguments)
|
34
|
-
results = send_request(targets, task, arguments, options)
|
35
|
-
|
36
|
-
process_run_results(targets, results)
|
37
|
-
rescue OrchestratorClient::ApiError => e
|
38
|
-
targets.map do |target|
|
39
|
-
Bolt::Result.new(target, error: e.data)
|
40
|
-
end
|
41
|
-
rescue StandardError => e
|
42
|
-
targets.map do |target|
|
43
|
-
Bolt::Result.from_exception(target, e)
|
44
|
-
end
|
45
|
-
end
|
46
|
-
|
47
|
-
def get(url)
|
48
|
-
response = @http.get(url)
|
49
|
-
if response.status != 200
|
50
|
-
raise Bolt::Error.new(response.body['msg'], response.body['kind'], response.body['details'])
|
51
|
-
end
|
52
|
-
response.body
|
53
|
-
end
|
54
|
-
|
55
|
-
def post_command(url, body)
|
56
|
-
response = @http.post(url, body)
|
57
|
-
if response.status != 202
|
58
|
-
raise Bolt::Error.new(response.body['msg'], response.body['kind'], response.body['details'])
|
59
|
-
end
|
60
|
-
response.body
|
61
|
-
end
|
62
|
-
|
63
|
-
def send_request(targets, task, arguments, options = {})
|
64
|
-
description = options[:description]
|
65
|
-
body = { task: task.name,
|
66
|
-
environment: @environment,
|
67
|
-
noop: arguments['_noop'],
|
68
|
-
params: arguments.reject { |k, _| k.start_with?('_') },
|
69
|
-
scope: {
|
70
|
-
nodes: targets.map(&:host)
|
71
|
-
} }
|
72
|
-
body[:description] = description if description
|
73
|
-
body[:plan_job] = @plan_job if @plan_job
|
74
|
-
|
75
|
-
url = post_command('internal/plan_task', body).dig('job', 'id')
|
76
|
-
|
77
|
-
job = get(url)
|
78
|
-
until %w[stopped finished failed].include?(job['state'])
|
79
|
-
sleep 1
|
80
|
-
job = get(url)
|
81
|
-
end
|
82
|
-
|
83
|
-
get(job.dig('nodes', 'id'))['items']
|
84
|
-
end
|
85
|
-
|
86
|
-
def process_run_results(targets, results)
|
87
|
-
targets_by_name = Hash[targets.map(&:host).zip(targets)]
|
88
|
-
results.map do |node_result|
|
89
|
-
target = targets_by_name[node_result['name']]
|
90
|
-
state = node_result['state']
|
91
|
-
result = node_result['result']
|
92
|
-
|
93
|
-
# If it's finished or already has a proper error simply pass it to the
|
94
|
-
# the result otherwise make sure an error is generated
|
95
|
-
if state == 'finished' || (result && result['_error'])
|
96
|
-
Bolt::Result.new(target, value: result)
|
97
|
-
elsif state == 'skipped'
|
98
|
-
Bolt::Result.new(
|
99
|
-
target,
|
100
|
-
value: { '_error' => {
|
101
|
-
'kind' => 'puppetlabs.tasks/skipped-node',
|
102
|
-
'msg' => "Node #{target.host} was skipped",
|
103
|
-
'details' => {}
|
104
|
-
} }
|
105
|
-
)
|
106
|
-
else
|
107
|
-
# Make a generic error with a unkown exit_code
|
108
|
-
Bolt::Result.for_task(target, result.to_json, '', 'unknown')
|
109
|
-
end
|
110
|
-
end
|
111
|
-
end
|
112
|
-
|
113
|
-
def run_task(targets, task, arguments, options = {})
|
114
|
-
run_task_job(targets, task, arguments, options)
|
115
|
-
end
|
116
|
-
|
117
|
-
def run_command(targets, command, options = {})
|
118
|
-
params = { 'command' => command }
|
119
|
-
run_task_job(targets, BOLT_COMMAND_TASK, params, options)
|
120
|
-
end
|
121
|
-
|
122
|
-
def run_script(targets, script, arguments, options = {})
|
123
|
-
content = File.open(script, &:read)
|
124
|
-
content = Base64.encode64(content)
|
125
|
-
params = {
|
126
|
-
'content' => content,
|
127
|
-
'arguments' => arguments,
|
128
|
-
'name' => Pathname(script).basename.to_s
|
129
|
-
}
|
130
|
-
callback ||= proc {}
|
131
|
-
results = run_task_job(targets, BOLT_SCRIPT_TASK, params, options, &callback)
|
132
|
-
results.map! { |result| unwrap_bolt_result(result.target, result) }
|
133
|
-
results.each do |result|
|
134
|
-
callback.call(type: :node_result, result: result)
|
135
|
-
end
|
136
|
-
end
|
137
|
-
|
138
|
-
def pack(directory)
|
139
|
-
# lazy-load expensive gem code
|
140
|
-
require 'minitar'
|
141
|
-
require 'zlib'
|
142
|
-
|
143
|
-
start_time = Time.now
|
144
|
-
io = StringIO.new
|
145
|
-
output = Minitar::Output.new(Zlib::GzipWriter.new(io))
|
146
|
-
Find.find(directory) do |file|
|
147
|
-
next unless File.file?(file)
|
148
|
-
|
149
|
-
tar_path = Pathname.new(file).relative_path_from(Pathname.new(directory))
|
150
|
-
@logger.debug("Packing #{file} to #{tar_path}")
|
151
|
-
stat = File.stat(file)
|
152
|
-
content = File.binread(file)
|
153
|
-
output.tar.add_file_simple(
|
154
|
-
tar_path.to_s,
|
155
|
-
data: content,
|
156
|
-
size: content.size,
|
157
|
-
mode: stat.mode & 0o777,
|
158
|
-
mtime: stat.mtime
|
159
|
-
)
|
160
|
-
end
|
161
|
-
|
162
|
-
duration = Time.now - start_time
|
163
|
-
@logger.debug("Packed upload in #{duration * 1000} ms")
|
164
|
-
|
165
|
-
output.close
|
166
|
-
io.string
|
167
|
-
ensure
|
168
|
-
# Closes both tar and sgz.
|
169
|
-
output&.close
|
170
|
-
end
|
171
|
-
|
172
|
-
def file_upload(targets, source, destination, options = {})
|
173
|
-
stat = File.stat(source)
|
174
|
-
content = if stat.directory?
|
175
|
-
pack(source)
|
176
|
-
else
|
177
|
-
File.open(source, &:read)
|
178
|
-
end
|
179
|
-
content = Base64.encode64(content)
|
180
|
-
mode = File.stat(source).mode
|
181
|
-
params = {
|
182
|
-
'path' => destination,
|
183
|
-
'content' => content,
|
184
|
-
'mode' => mode,
|
185
|
-
'directory' => stat.directory?
|
186
|
-
}
|
187
|
-
results = run_task_job(targets, BOLT_UPLOAD_TASK, params, options)
|
188
|
-
results.map! do |result|
|
189
|
-
if result.error_hash
|
190
|
-
result
|
191
|
-
else
|
192
|
-
Bolt::Result.for_upload(result.target, source, destination)
|
193
|
-
end
|
194
|
-
end
|
195
|
-
end
|
196
|
-
|
197
|
-
# Unwraps any Sensitive data in an arguments Hash, so the plain-text is passed
|
198
|
-
# to the Task/Script.
|
199
|
-
#
|
200
|
-
# This works on deeply nested data structures composed of Hashes, Arrays, and
|
201
|
-
# and plain-old data types (int, string, etc).
|
202
|
-
def unwrap_sensitive_args(arguments)
|
203
|
-
# Skip this if Puppet isn't loaded
|
204
|
-
return arguments unless defined?(Puppet::Pops::Types::PSensitiveType::Sensitive)
|
205
|
-
|
206
|
-
case arguments
|
207
|
-
when Array
|
208
|
-
# iterate over the array, unwrapping all elements
|
209
|
-
arguments.map { |x| unwrap_sensitive_args(x) }
|
210
|
-
when Hash
|
211
|
-
# iterate over the arguments hash and unwrap all keys and values
|
212
|
-
arguments.each_with_object({}) { |(k, v), h|
|
213
|
-
h[unwrap_sensitive_args(k)] = unwrap_sensitive_args(v)
|
214
|
-
}
|
215
|
-
when Puppet::Pops::Types::PSensitiveType::Sensitive
|
216
|
-
# this value is Sensitive, unwrap it
|
217
|
-
unwrap_sensitive_args(arguments.unwrap)
|
218
|
-
else
|
219
|
-
# unknown data type, just return it
|
220
|
-
arguments
|
221
|
-
end
|
222
|
-
end
|
223
|
-
|
224
|
-
# run_task generates a result that makes sense for a generic task which
|
225
|
-
# needs to be unwrapped to extract stdout/stderr/exitcode.
|
226
|
-
#
|
227
|
-
def unwrap_bolt_result(target, result)
|
228
|
-
if result.error_hash
|
229
|
-
# something went wrong return the failure
|
230
|
-
return result
|
231
|
-
end
|
232
|
-
|
233
|
-
Bolt::Result.for_command(target, result.value['stdout'], result.value['stderr'], result.value['exit_code'])
|
234
|
-
end
|
235
|
-
|
236
|
-
def connected?(targets)
|
237
|
-
response = @http.post('inventory', nodes: targets.map(&:host))
|
238
|
-
response.body['items'].all? { |node| node['connected'] }
|
239
|
-
end
|
240
|
-
end
|
241
|
-
end
|
@@ -1,29 +0,0 @@
|
|
1
|
-
{
|
2
|
-
"$schema": "http://json-schema.org/draft-04/schema#",
|
3
|
-
"title": "run_plan request",
|
4
|
-
"description": "POST plan/run request schema",
|
5
|
-
"type": "object",
|
6
|
-
"properties": {
|
7
|
-
"plan_name": {
|
8
|
-
"type": "string",
|
9
|
-
"description": "Name of the plan"
|
10
|
-
},
|
11
|
-
"job_id": {
|
12
|
-
"type": "string",
|
13
|
-
"description": "The job ID initialized in Orchestrator"
|
14
|
-
},
|
15
|
-
"environment": {
|
16
|
-
"type": "string",
|
17
|
-
"description": "Environment used for plan execution"
|
18
|
-
},
|
19
|
-
"description": {
|
20
|
-
"type": "string",
|
21
|
-
"description": "Describes this execution of the plan"
|
22
|
-
},
|
23
|
-
"params": {
|
24
|
-
"type": "object",
|
25
|
-
"description": "JSON formatted parameters to be provided to plan"
|
26
|
-
}
|
27
|
-
},
|
28
|
-
"required": ["plan_name", "job_id", "params"]
|
29
|
-
}
|