bolt 2.11.0 → 2.15.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/Puppetfile +1 -1
- data/bolt-modules/boltlib/lib/puppet/datatypes/resourceinstance.rb +3 -2
- data/bolt-modules/boltlib/lib/puppet/functions/apply_prep.rb +1 -1
- data/bolt-modules/boltlib/lib/puppet/functions/get_resources.rb +1 -1
- data/bolt-modules/boltlib/lib/puppet/functions/resource.rb +52 -0
- data/bolt-modules/boltlib/lib/puppet/functions/run_plan.rb +65 -0
- data/bolt-modules/boltlib/lib/puppet/functions/run_task.rb +4 -2
- data/bolt-modules/boltlib/lib/puppet/functions/run_task_with.rb +8 -2
- data/bolt-modules/boltlib/lib/puppet/functions/set_resources.rb +65 -43
- data/bolt-modules/file/lib/puppet/functions/file/exists.rb +1 -1
- data/bolt-modules/file/lib/puppet/functions/file/read.rb +1 -1
- data/bolt-modules/file/lib/puppet/functions/file/readable.rb +1 -1
- data/lib/bolt/analytics.rb +21 -2
- data/lib/bolt/applicator.rb +19 -7
- data/lib/bolt/apply_inventory.rb +4 -0
- data/lib/bolt/apply_target.rb +4 -0
- data/lib/bolt/bolt_option_parser.rb +4 -3
- data/lib/bolt/catalog.rb +81 -68
- data/lib/bolt/cli.rb +16 -5
- data/lib/bolt/config.rb +216 -75
- data/lib/bolt/config/transport/ssh.rb +130 -91
- data/lib/bolt/executor.rb +14 -1
- data/lib/bolt/inventory/group.rb +1 -1
- data/lib/bolt/inventory/inventory.rb +4 -0
- data/lib/bolt/inventory/target.rb +4 -0
- data/lib/bolt/outputter.rb +3 -0
- data/lib/bolt/outputter/rainbow.rb +80 -0
- data/lib/bolt/pal.rb +3 -0
- data/lib/bolt/project.rb +48 -11
- data/lib/bolt/resource_instance.rb +10 -3
- data/lib/bolt/shell/powershell/snippets.rb +8 -0
- data/lib/bolt/transport/local/connection.rb +2 -1
- data/lib/bolt/transport/ssh/connection.rb +35 -0
- data/lib/bolt/version.rb +1 -1
- data/lib/bolt_spec/bolt_context.rb +1 -1
- data/lib/bolt_spec/run.rb +1 -1
- metadata +21 -5
data/lib/bolt/applicator.rb
CHANGED
@@ -14,14 +14,16 @@ require 'open3'
|
|
14
14
|
|
15
15
|
module Bolt
|
16
16
|
class Applicator
|
17
|
-
def initialize(inventory, executor, modulepath, plugin_dirs,
|
17
|
+
def initialize(inventory, executor, modulepath, plugin_dirs, project,
|
18
|
+
pdb_client, hiera_config, max_compiles, apply_settings)
|
18
19
|
# lazy-load expensive gem code
|
19
20
|
require 'concurrent'
|
20
21
|
|
21
22
|
@inventory = inventory
|
22
23
|
@executor = executor
|
23
|
-
@modulepath = modulepath
|
24
|
+
@modulepath = modulepath || []
|
24
25
|
@plugin_dirs = plugin_dirs
|
26
|
+
@project = project
|
25
27
|
@pdb_client = pdb_client
|
26
28
|
@hiera_config = hiera_config ? validate_hiera_config(hiera_config) : nil
|
27
29
|
@apply_settings = apply_settings || {}
|
@@ -104,6 +106,16 @@ module Bolt
|
|
104
106
|
out, err, stat = Open3.capture3('ruby', bolt_catalog_exe, 'compile', stdin_data: catalog_input.to_json)
|
105
107
|
ENV['PATH'] = old_path
|
106
108
|
|
109
|
+
# If bolt_catalog does not return valid JSON, we should print stderr to
|
110
|
+
# see what happened
|
111
|
+
print_logs = stat.success?
|
112
|
+
result = begin
|
113
|
+
JSON.parse(out)
|
114
|
+
rescue JSON::ParserError
|
115
|
+
print_logs = true
|
116
|
+
{ 'message' => "Something's gone terribly wrong! STDERR is logged." }
|
117
|
+
end
|
118
|
+
|
107
119
|
# Any messages logged by Puppet will be on stderr as JSON hashes, so we
|
108
120
|
# parse those and store them here. Any message on stderr that is not
|
109
121
|
# properly JSON formatted is assumed to be an error message. If
|
@@ -117,17 +129,15 @@ module Bolt
|
|
117
129
|
{ 'level' => 'err', 'message' => line }
|
118
130
|
end
|
119
131
|
|
120
|
-
|
121
|
-
if stat.success?
|
132
|
+
if print_logs
|
122
133
|
logs.each do |log|
|
123
134
|
bolt_level = Bolt::Util::PuppetLogLevel::MAPPING[log['level'].to_sym]
|
124
135
|
message = log['message'].chomp
|
125
136
|
@logger.send(bolt_level, "#{target.name}: #{message}")
|
126
137
|
end
|
127
|
-
result
|
128
|
-
else
|
129
|
-
raise ApplyError.new(target.name, result['message'])
|
130
138
|
end
|
139
|
+
raise ApplyError.new(target.name, result['message']) unless stat.success?
|
140
|
+
result
|
131
141
|
end
|
132
142
|
|
133
143
|
def validate_hiera_config(hiera_config)
|
@@ -144,6 +154,7 @@ module Bolt
|
|
144
154
|
|
145
155
|
def apply(args, apply_body, scope)
|
146
156
|
raise(ArgumentError, 'apply requires a TargetSpec') if args.empty?
|
157
|
+
raise(ArgumentError, 'apply requires at least one statement in the apply block') if apply_body.nil?
|
147
158
|
type0 = Puppet.lookup(:pal_script_compiler).type('TargetSpec')
|
148
159
|
Puppet::Pal.assert_type(type0, args[0], 'apply targets')
|
149
160
|
|
@@ -188,6 +199,7 @@ module Bolt
|
|
188
199
|
scope = {
|
189
200
|
code_ast: ast,
|
190
201
|
modulepath: @modulepath,
|
202
|
+
project: @project.to_h,
|
191
203
|
pdb_config: @pdb_client.config.to_hash,
|
192
204
|
hiera_config: @hiera_config,
|
193
205
|
plan_vars: plan_vars,
|
data/lib/bolt/apply_inventory.rb
CHANGED
data/lib/bolt/apply_target.rb
CHANGED
@@ -20,7 +20,7 @@ module Bolt
|
|
20
20
|
def get_help_text(subcommand, action = nil)
|
21
21
|
case subcommand
|
22
22
|
when 'apply'
|
23
|
-
{ flags: ACTION_OPTS + %w[noop execute compile-concurrency],
|
23
|
+
{ flags: ACTION_OPTS + %w[noop execute compile-concurrency hiera-config],
|
24
24
|
banner: APPLY_HELP }
|
25
25
|
when 'command'
|
26
26
|
case action
|
@@ -172,13 +172,14 @@ module Bolt
|
|
172
172
|
apply
|
173
173
|
|
174
174
|
USAGE
|
175
|
-
bolt apply
|
175
|
+
bolt apply [manifest.pp] [options]
|
176
176
|
|
177
177
|
DESCRIPTION
|
178
178
|
Apply Puppet manifest code on the specified targets.
|
179
179
|
|
180
180
|
EXAMPLES
|
181
|
-
bolt apply manifest.pp
|
181
|
+
bolt apply manifest.pp -t target
|
182
|
+
bolt apply -e "file { '/etc/puppetlabs': ensure => present }" -t target
|
182
183
|
HELP
|
183
184
|
|
184
185
|
COMMAND_HELP = <<~HELP
|
data/lib/bolt/catalog.rb
CHANGED
@@ -1,10 +1,10 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'bolt/apply_inventory'
|
3
4
|
require 'bolt/apply_target'
|
4
5
|
require 'bolt/config'
|
5
6
|
require 'bolt/error'
|
6
7
|
require 'bolt/inventory'
|
7
|
-
require 'bolt/apply_inventory'
|
8
8
|
require 'bolt/pal'
|
9
9
|
require 'bolt/puppetdb'
|
10
10
|
require 'bolt/util'
|
@@ -19,7 +19,7 @@ module Bolt
|
|
19
19
|
@log_level = log_level
|
20
20
|
end
|
21
21
|
|
22
|
-
def with_puppet_settings(
|
22
|
+
def with_puppet_settings(overrides = {})
|
23
23
|
Dir.mktmpdir('bolt') do |dir|
|
24
24
|
cli = []
|
25
25
|
Puppet::Settings::REQUIRED_APP_SETTINGS.each do |setting|
|
@@ -31,7 +31,9 @@ module Bolt
|
|
31
31
|
Puppet.settings.override_default(:vendormoduledir, '')
|
32
32
|
|
33
33
|
Puppet.initialize_settings(cli)
|
34
|
-
|
34
|
+
overrides.each do |setting, value|
|
35
|
+
Puppet.settings[setting] = value
|
36
|
+
end
|
35
37
|
|
36
38
|
# Use a special logdest that serializes all log messages and their level to stderr.
|
37
39
|
Puppet::Util::Log.newdestination(:stderr)
|
@@ -54,80 +56,51 @@ module Bolt
|
|
54
56
|
end
|
55
57
|
|
56
58
|
def compile_catalog(request)
|
57
|
-
pal_main = request['code_ast'] || request['code_string']
|
58
|
-
target = request['target']
|
59
59
|
pdb_client = Bolt::PuppetDB::Client.new(Bolt::PuppetDB::Config.new(request['pdb_config']))
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
60
|
+
project = request['project'] || {}
|
61
|
+
bolt_project = Struct.new(:name, :path).new(project['name'], project['path']) unless project.empty?
|
62
|
+
inv = Bolt::ApplyInventory.new(request['config'])
|
63
|
+
puppet_overrides = {
|
64
|
+
bolt_pdb_client: pdb_client,
|
65
|
+
bolt_inventory: inv,
|
66
|
+
bolt_project: bolt_project
|
67
|
+
}
|
68
|
+
|
69
|
+
# Facts will be set by the catalog compiler, so we need to ensure
|
70
|
+
# that any plan or target variables with the same name are not
|
71
|
+
# passed into the apply block to avoid a redefinition error.
|
72
|
+
# Filter out plan and target vars separately and raise a Puppet
|
73
|
+
# warning if there are any collisions for either. Puppet warning
|
74
|
+
# is the only way to log a message that will make it back to Bolt
|
75
|
+
# to be printed.
|
76
|
+
target = request['target']
|
77
|
+
plan_vars = shadow_vars('plan', request['plan_vars'], target['facts'])
|
78
|
+
target_vars = shadow_vars('target', target['variables'], target['facts'])
|
79
|
+
topscope_vars = target_vars.merge(plan_vars)
|
80
|
+
env_conf = { modulepath: request['modulepath'],
|
81
|
+
facts: target['facts'],
|
82
|
+
variables: topscope_vars }
|
83
|
+
|
84
|
+
puppet_settings = {
|
85
|
+
node_name_value: target['name'],
|
86
|
+
hiera_config: request['hiera_config']
|
87
|
+
}
|
88
|
+
|
89
|
+
with_puppet_settings(puppet_settings) do
|
67
90
|
Puppet::Pal.in_tmp_environment('bolt_catalog', env_conf) do |pal|
|
68
|
-
|
69
|
-
Puppet.override(bolt_pdb_client: pdb_client,
|
70
|
-
bolt_inventory: inv) do
|
91
|
+
Puppet.override(puppet_overrides) do
|
71
92
|
Puppet.lookup(:pal_current_node).trusted_data = target['trusted']
|
72
93
|
pal.with_catalog_compiler do |compiler|
|
73
|
-
|
74
|
-
# loaders are initialized for loading
|
75
|
-
plan_vars = Puppet::Pops::Serialization::FromDataConverter.convert(request['plan_vars'])
|
76
|
-
|
77
|
-
# Facts will be set by the catalog compiler, so we need to ensure
|
78
|
-
# that any plan or target variables with the same name are not
|
79
|
-
# passed into the apply block to avoid a redefinition error.
|
80
|
-
# Filter out plan and target vars separately and raise a Puppet
|
81
|
-
# warning if there are any collisions for either. Puppet warning
|
82
|
-
# is the only way to log a message that will make it back to Bolt
|
83
|
-
# to be printed.
|
84
|
-
pv_collisions, pv_filtered = plan_vars.partition do |k, _|
|
85
|
-
target['facts'].keys.include?(k)
|
86
|
-
end.map(&:to_h)
|
87
|
-
unless pv_collisions.empty?
|
88
|
-
print_pv = pv_collisions.keys.map { |k| "$#{k}" }.join(', ')
|
89
|
-
plural = pv_collisions.keys.length == 1 ? '' : 's'
|
90
|
-
Puppet.warning("Plan variable#{plural} #{print_pv} will be overridden by fact#{plural} " \
|
91
|
-
"of the same name in the apply block")
|
92
|
-
end
|
93
|
-
|
94
|
-
tv_collisions, tv_filtered = target['variables'].partition do |k, _|
|
95
|
-
target['facts'].keys.include?(k)
|
96
|
-
end.map(&:to_h)
|
97
|
-
unless tv_collisions.empty?
|
98
|
-
print_tv = tv_collisions.keys.map { |k| "$#{k}" }.join(', ')
|
99
|
-
plural = tv_collisions.keys.length == 1 ? '' : 's'
|
100
|
-
Puppet.warning("Target variable#{plural} #{print_tv} " \
|
101
|
-
"will be overridden by fact#{plural} of the same name in the apply block")
|
102
|
-
end
|
103
|
-
|
104
|
-
pal.send(:add_variables, compiler.send(:topscope), tv_filtered.merge(pv_filtered))
|
105
|
-
|
94
|
+
options = request['puppet_config'] || {}
|
106
95
|
# Configure language strictness in the CatalogCompiler. We want Bolt to be able
|
107
96
|
# to compile most Puppet 4+ manifests, so we default to allowing deprecated functions.
|
108
97
|
Puppet[:strict] = options['strict'] || :warning
|
109
98
|
Puppet[:strict_variables] = options['strict_variables'] || false
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
# plan. In that case, we need to discover the definitions (which
|
114
|
-
# would ordinarily be stored on the Program) and construct a Program object.
|
115
|
-
unless ast.is_a?(Puppet::Pops::Model::Program)
|
116
|
-
# Node definitions must be at the top level of the apply block.
|
117
|
-
# That means the apply body either a) consists of just a
|
118
|
-
# NodeDefinition, b) consists of a BlockExpression which may
|
119
|
-
# contain NodeDefinitions, or c) doesn't contain NodeDefinitions.
|
120
|
-
definitions = if ast.is_a?(Puppet::Pops::Model::BlockExpression)
|
121
|
-
ast.statements.select { |st| st.is_a?(Puppet::Pops::Model::NodeDefinition) }
|
122
|
-
elsif ast.is_a?(Puppet::Pops::Model::NodeDefinition)
|
123
|
-
[ast]
|
124
|
-
else
|
125
|
-
[]
|
126
|
-
end
|
127
|
-
ast = Puppet::Pops::Model::Factory.PROGRAM(ast, definitions, ast.locator).model
|
128
|
-
end
|
99
|
+
|
100
|
+
pal_main = request['code_ast'] || request['code_string']
|
101
|
+
ast = build_program(pal_main)
|
129
102
|
compiler.evaluate(ast)
|
130
|
-
compiler.
|
103
|
+
compiler.evaluate_ast_node
|
131
104
|
compiler.compile_additions
|
132
105
|
compiler.with_json_encoding(&:encode)
|
133
106
|
end
|
@@ -135,5 +108,45 @@ module Bolt
|
|
135
108
|
end
|
136
109
|
end
|
137
110
|
end
|
111
|
+
|
112
|
+
# Warn and remove variables that will be shadowed by facts of the same
|
113
|
+
# name, which are set in scope earlier.
|
114
|
+
def shadow_vars(type, vars, facts)
|
115
|
+
collisions, valid = vars.partition do |k, _|
|
116
|
+
facts.include?(k)
|
117
|
+
end
|
118
|
+
if collisions.any?
|
119
|
+
names = collisions.map { |k, _| "$#{k}" }.join(', ')
|
120
|
+
plural = collisions.length == 1 ? '' : 's'
|
121
|
+
Puppet.warning("#{type.capitalize} variable#{plural} #{names} will be overridden by fact#{plural} " \
|
122
|
+
"of the same name in the apply block")
|
123
|
+
end
|
124
|
+
valid.to_h
|
125
|
+
end
|
126
|
+
|
127
|
+
def build_program(code)
|
128
|
+
ast = Puppet::Pops::Serialization::FromDataConverter.convert(code)
|
129
|
+
|
130
|
+
# This will be a Program when running via `bolt apply`, but will
|
131
|
+
# only be a subset of the AST when compiling an apply block in a
|
132
|
+
# plan. In that case, we need to discover the definitions (which
|
133
|
+
# would ordinarily be stored on the Program) and construct a Program object.
|
134
|
+
if ast.is_a?(Puppet::Pops::Model::Program)
|
135
|
+
ast
|
136
|
+
else
|
137
|
+
# Node definitions must be at the top level of the apply block.
|
138
|
+
# That means the apply body either a) consists of just a
|
139
|
+
# NodeDefinition, b) consists of a BlockExpression which may
|
140
|
+
# contain NodeDefinitions, or c) doesn't contain NodeDefinitions.
|
141
|
+
definitions = if ast.is_a?(Puppet::Pops::Model::BlockExpression)
|
142
|
+
ast.statements.select { |st| st.is_a?(Puppet::Pops::Model::NodeDefinition) }
|
143
|
+
elsif ast.is_a?(Puppet::Pops::Model::NodeDefinition)
|
144
|
+
[ast]
|
145
|
+
else
|
146
|
+
[]
|
147
|
+
end
|
148
|
+
Puppet::Pops::Model::Factory.PROGRAM(ast, definitions, ast.locator).model
|
149
|
+
end
|
150
|
+
end
|
138
151
|
end
|
139
152
|
end
|
data/lib/bolt/cli.rb
CHANGED
@@ -115,7 +115,7 @@ module Bolt
|
|
115
115
|
Bolt::Config.from_file(options[:configfile], options)
|
116
116
|
else
|
117
117
|
project = if options[:boltdir]
|
118
|
-
Bolt::Project.
|
118
|
+
Bolt::Project.create_project(options[:boltdir])
|
119
119
|
else
|
120
120
|
Bolt::Project.find_boltdir(Dir.pwd)
|
121
121
|
end
|
@@ -392,7 +392,7 @@ module Bolt
|
|
392
392
|
end
|
393
393
|
code = apply_manifest(options[:code], options[:targets], options[:object], options[:noop])
|
394
394
|
else
|
395
|
-
executor = Bolt::Executor.new(config.concurrency, analytics, options[:noop])
|
395
|
+
executor = Bolt::Executor.new(config.concurrency, analytics, options[:noop], config.modified_concurrency)
|
396
396
|
targets = options[:targets]
|
397
397
|
|
398
398
|
results = nil
|
@@ -512,7 +512,7 @@ module Bolt
|
|
512
512
|
params: plan_arguments }
|
513
513
|
plan_context[:description] = options[:description] if options[:description]
|
514
514
|
|
515
|
-
executor = Bolt::Executor.new(config.concurrency, analytics, options[:noop])
|
515
|
+
executor = Bolt::Executor.new(config.concurrency, analytics, options[:noop], config.modified_concurrency)
|
516
516
|
if options.fetch(:format, 'human') == 'human'
|
517
517
|
executor.subscribe(outputter)
|
518
518
|
else
|
@@ -537,7 +537,18 @@ module Bolt
|
|
537
537
|
Puppet[:tasks] = false
|
538
538
|
ast = pal.parse_manifest(code, filename)
|
539
539
|
|
540
|
-
|
540
|
+
if defined?(ast.body) &&
|
541
|
+
(ast.body.is_a?(Puppet::Pops::Model::HostClassDefinition) ||
|
542
|
+
ast.body.is_a?(Puppet::Pops::Model::ResourceTypeDefinition))
|
543
|
+
message = "Manifest only contains definitions and will result in no changes on the targets. "\
|
544
|
+
"Definitions must be declared for their resources to be applied. You can read more "\
|
545
|
+
"about defining and declaring classes and types in the Puppet documentation at "\
|
546
|
+
"https://puppet.com/docs/puppet/latest/lang_classes.html and "\
|
547
|
+
"https://puppet.com/docs/puppet/latest/lang_defined_types.html"
|
548
|
+
@logger.warn(message)
|
549
|
+
end
|
550
|
+
|
551
|
+
executor = Bolt::Executor.new(config.concurrency, analytics, noop, config.modified_concurrency)
|
541
552
|
executor.subscribe(outputter) if options.fetch(:format, 'human') == 'human'
|
542
553
|
executor.subscribe(log_outputter)
|
543
554
|
# apply logging looks like plan logging, so tell the outputter we're in a
|
@@ -760,7 +771,7 @@ module Bolt
|
|
760
771
|
end
|
761
772
|
|
762
773
|
def pal
|
763
|
-
project = config.project.
|
774
|
+
project = config.project.project_file? ? config.project : nil
|
764
775
|
@pal ||= Bolt::PAL.new(config.modulepath,
|
765
776
|
config.hiera_config,
|
766
777
|
config.project.resource_types,
|
data/lib/bolt/config.rb
CHANGED
@@ -23,8 +23,13 @@ module Bolt
|
|
23
23
|
end
|
24
24
|
|
25
25
|
class Config
|
26
|
-
attr_reader :config_files, :warnings, :data, :transports, :project
|
26
|
+
attr_reader :config_files, :warnings, :data, :transports, :project, :modified_concurrency
|
27
27
|
|
28
|
+
BOLT_CONFIG_NAME = 'bolt.yaml'
|
29
|
+
BOLT_DEFAULTS_NAME = 'bolt-defaults.yaml'
|
30
|
+
|
31
|
+
# Transport config classes. Used to load default transport config which
|
32
|
+
# gets passed along to the inventory.
|
28
33
|
TRANSPORT_CONFIG = {
|
29
34
|
'ssh' => Bolt::Config::Transport::SSH,
|
30
35
|
'winrm' => Bolt::Config::Transport::WinRM,
|
@@ -34,45 +39,84 @@ module Bolt
|
|
34
39
|
'remote' => Bolt::Config::Transport::Remote
|
35
40
|
}.freeze
|
36
41
|
|
37
|
-
#
|
38
|
-
#
|
39
|
-
|
42
|
+
# Options that configure Bolt. These options are used in bolt.yaml and
|
43
|
+
# bolt-defaults.yaml.
|
44
|
+
BOLT_CONFIG = {
|
45
|
+
"color" => "Whether to use colored output when printing messages to the console.",
|
46
|
+
"compile-concurrency" => "The maximum number of simultaneous manifest block compiles.",
|
47
|
+
"concurrency" => "The number of threads to use when executing on remote targets.",
|
48
|
+
"format" => "The format to use when printing results. Options are `human` and `json`.",
|
49
|
+
"plugin_hooks" => "Which plugins a specific hook should use.",
|
50
|
+
"plugins" => "A map of plugins and their configuration data.",
|
51
|
+
"puppetdb" => "A map containing options for configuring the Bolt PuppetDB client.",
|
52
|
+
"puppetfile" => "A map containing options for the `bolt puppetfile install` command.",
|
53
|
+
"save-rerun" => "Whether to update `.rerun.json` in the Bolt project directory. If "\
|
54
|
+
"your target names include passwords, set this value to `false` to avoid "\
|
55
|
+
"writing passwords to disk."
|
56
|
+
}.freeze
|
57
|
+
|
58
|
+
# These options are only available to bolt-defaults.yaml.
|
59
|
+
DEFAULTS_CONFIG = {
|
60
|
+
"inventory-config" => "A map of default configuration options for the inventory. This includes options "\
|
61
|
+
"for setting the default transport to use when connecting to targets, as well as "\
|
62
|
+
"options for configuring the default behavior of each transport."
|
63
|
+
}.freeze
|
64
|
+
|
65
|
+
# Options that configure the inventory, specifically the default transport
|
66
|
+
# used by targets and the transports themselves. These options are used in
|
67
|
+
# bolt.yaml, inventory.yaml, and under the inventory-config key in
|
68
|
+
# bolt-defaults.yaml.
|
69
|
+
INVENTORY_CONFIG = {
|
70
|
+
"transport" => "The default transport to use when the transport for a target is not specified in the URI.",
|
71
|
+
"docker" => "A map of configuration options for the docker transport.",
|
72
|
+
"local" => "A map of configuration options for the local transport.",
|
73
|
+
"pcp" => "A map of configuration options for the pcp transport.",
|
74
|
+
"remote" => "A map of configuration options for the remote transport.",
|
75
|
+
"ssh" => "A map of configuration options for the ssh transport.",
|
76
|
+
"winrm" => "A map of configuration options for the winrm transport."
|
77
|
+
}.freeze
|
78
|
+
|
79
|
+
# Options that configure the project, such as paths to files used for a
|
80
|
+
# specific project. These settings are used in bolt.yaml and bolt-project.yaml.
|
81
|
+
PROJECT_CONFIG = {
|
40
82
|
"apply_settings" => "A map of Puppet settings to use when applying Puppet code",
|
41
|
-
"color" => "Whether to use colored output when printing messages to the console.",
|
42
|
-
"compile-concurrency" => "The maximum number of simultaneous manifest block compiles.",
|
43
|
-
"concurrency" => "The number of threads to use when executing on remote targets.",
|
44
|
-
"format" => "The format to use when printing results. Options are `human` and `json`.",
|
45
83
|
"hiera-config" => "The path to your Hiera config.",
|
46
84
|
"inventoryfile" => "The path to a structured data inventory file used to refer to groups of "\
|
47
85
|
"targets on the command line and from plans.",
|
48
86
|
"log" => "The configuration of the logfile output. Configuration can be set for "\
|
49
87
|
"`console` and the path to a log file, such as `~/.puppetlabs/bolt/debug.log`.",
|
50
|
-
"modulepath" => "
|
51
|
-
"of directories or a string containing a list of directories separated by the "\
|
52
|
-
"OS-specific PATH separator.",
|
53
|
-
"plugin_hooks" => "Which plugins a specific hook should use.",
|
54
|
-
"plugins" => "A map of plugins and their configuration data.",
|
55
|
-
"puppetdb" => "A map containing options for configuring the Bolt PuppetDB client.",
|
56
|
-
"puppetfile" => "A map containing options for the `bolt puppetfile install` command.",
|
57
|
-
"save-rerun" => "Whether to update `.rerun.json` in the Bolt project directory. If "\
|
58
|
-
"your target names include passwords, set this value to `false` to avoid "\
|
59
|
-
"writing passwords to disk.",
|
60
|
-
"transport" => "The default transport to use when the transport for a target is not "\
|
61
|
-
"specified in the URL or inventory.",
|
88
|
+
"modulepath" => "An array of directories that Bolt loads content (e.g. plans and tasks) from.",
|
62
89
|
"trusted-external-command" => "The path to an executable on the Bolt controller that can produce "\
|
63
90
|
"external trusted facts. **External trusted facts are experimental in both "\
|
64
91
|
"Puppet and Bolt and this API may change or be removed.**"
|
65
92
|
}.freeze
|
66
93
|
|
94
|
+
# A combined map of all configuration options that can be set in this class.
|
95
|
+
# Includes all options except 'inventory-config', which is munged when loading
|
96
|
+
# a bolt-defaults.yaml file.
|
97
|
+
OPTIONS = BOLT_CONFIG.merge(INVENTORY_CONFIG).merge(PROJECT_CONFIG).freeze
|
98
|
+
|
99
|
+
# Default values for select options. These do not set the default values in Bolt
|
100
|
+
# and are only used for documentation.
|
67
101
|
DEFAULT_OPTIONS = {
|
68
|
-
"color"
|
102
|
+
"color" => true,
|
69
103
|
"compile-concurrency" => "Number of cores",
|
70
|
-
"concurrency"
|
71
|
-
"format"
|
72
|
-
"hiera-config"
|
73
|
-
"inventoryfile"
|
74
|
-
"modulepath"
|
75
|
-
"save-rerun"
|
104
|
+
"concurrency" => "100 or one-seventh of the ulimit, whichever is lower",
|
105
|
+
"format" => "human",
|
106
|
+
"hiera-config" => "Boltdir/hiera.yaml",
|
107
|
+
"inventoryfile" => "Boltdir/inventory.yaml",
|
108
|
+
"modulepath" => ["Boltdir/modules", "Boltdir/site-modules", "Boltdir/site"],
|
109
|
+
"save-rerun" => true,
|
110
|
+
"transport" => "ssh"
|
111
|
+
}.freeze
|
112
|
+
|
113
|
+
PUPPETDB_OPTIONS = {
|
114
|
+
"cacert" => "The path to the ca certificate for PuppetDB.",
|
115
|
+
"cert" => "The path to the client certificate file to use for authentication.",
|
116
|
+
"key" => "The private key for the certificate.",
|
117
|
+
"server_urls" => "An array containing the PuppetDB host to connect to. Include the protocol `https` and "\
|
118
|
+
"the port, which is usually `8081`. For example, `https://my-master.example.com:8081`.",
|
119
|
+
"token" => "The path to the PE RBAC Token."
|
76
120
|
}.freeze
|
77
121
|
|
78
122
|
PUPPETFILE_OPTIONS = {
|
@@ -106,61 +150,154 @@ module Bolt
|
|
106
150
|
DEFAULT_DEFAULT_CONCURRENCY = 100
|
107
151
|
|
108
152
|
def self.default
|
109
|
-
new(Bolt::Project.
|
153
|
+
new(Bolt::Project.create_project('.'), {})
|
110
154
|
end
|
111
155
|
|
112
156
|
def self.from_project(project, overrides = {})
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
157
|
+
conf = if project.project_file == project.config_file
|
158
|
+
project.data
|
159
|
+
else
|
160
|
+
Bolt::Util.read_optional_yaml_hash(project.config_file, 'config')
|
161
|
+
end
|
117
162
|
|
118
|
-
data = load_defaults.push(
|
163
|
+
data = load_defaults(project).push(
|
164
|
+
filepath: project.config_file,
|
165
|
+
data: conf,
|
166
|
+
warnings: []
|
167
|
+
)
|
119
168
|
|
120
169
|
new(project, data, overrides)
|
121
170
|
end
|
122
171
|
|
123
172
|
def self.from_file(configfile, overrides = {})
|
124
|
-
project = Bolt::Project.
|
173
|
+
project = Bolt::Project.create_project(Pathname.new(configfile).expand_path.dirname)
|
125
174
|
|
126
|
-
|
175
|
+
conf = if project.project_file == project.config_file
|
176
|
+
project.data
|
177
|
+
else
|
178
|
+
Bolt::Util.read_yaml_hash(configfile, 'config')
|
179
|
+
end
|
180
|
+
|
181
|
+
data = load_defaults(project).push(
|
127
182
|
filepath: project.config_file,
|
128
|
-
data:
|
129
|
-
|
130
|
-
|
183
|
+
data: conf,
|
184
|
+
warnings: []
|
185
|
+
)
|
131
186
|
|
132
187
|
new(project, data, overrides)
|
133
188
|
end
|
134
189
|
|
135
|
-
def self.
|
190
|
+
def self.system_path
|
136
191
|
# Lazy-load expensive gem code
|
137
192
|
require 'win32/dir' if Bolt::Util.windows?
|
138
193
|
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
194
|
+
if Bolt::Util.windows?
|
195
|
+
Pathname.new(File.join(Dir::COMMON_APPDATA, 'PuppetLabs', 'bolt', 'etc'))
|
196
|
+
else
|
197
|
+
Pathname.new(File.join('/etc', 'puppetlabs', 'bolt'))
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
def self.user_path
|
202
|
+
Pathname.new(File.expand_path(File.join('~', '.puppetlabs', 'etc', 'bolt')))
|
203
|
+
rescue StandardError
|
204
|
+
nil
|
205
|
+
end
|
206
|
+
|
207
|
+
# Loads a 'bolt-defaults.yaml' file, which contains default configuration that applies to all
|
208
|
+
# projects. This file does not allow project-specific configuration such as 'hiera-config' and
|
209
|
+
# 'inventoryfile', and nests all default inventory configuration under an 'inventory-config' key.
|
210
|
+
def self.load_bolt_defaults_yaml(dir)
|
211
|
+
filepath = dir + BOLT_DEFAULTS_NAME
|
212
|
+
data = Bolt::Util.read_yaml_hash(filepath, 'config')
|
213
|
+
warnings = []
|
214
|
+
|
215
|
+
# Warn if 'bolt.yaml' detected in same directory.
|
216
|
+
if File.exist?(bolt_yaml = dir + BOLT_CONFIG_NAME)
|
217
|
+
warnings.push(
|
218
|
+
msg: "Detected multiple configuration files: ['#{bolt_yaml}', '#{filepath}']. '#{bolt_yaml}' "\
|
219
|
+
"will be ignored."
|
220
|
+
)
|
221
|
+
end
|
222
|
+
|
223
|
+
# Remove project-specific config such as hiera-config, etc.
|
224
|
+
project_config = data.slice(*PROJECT_CONFIG.keys)
|
225
|
+
|
226
|
+
if project_config.any?
|
227
|
+
data.reject! { |key, _| project_config.include?(key) }
|
228
|
+
warnings.push(
|
229
|
+
msg: "Unsupported project configuration detected in '#{filepath}': #{project_config.keys}. "\
|
230
|
+
"Project configuration should be set in 'bolt-project.yaml'."
|
231
|
+
)
|
232
|
+
end
|
233
|
+
|
234
|
+
# Remove top-level transport config such as transport, ssh, etc.
|
235
|
+
transport_config = data.slice(*INVENTORY_CONFIG.keys)
|
236
|
+
|
237
|
+
if transport_config.any?
|
238
|
+
data.reject! { |key, _| transport_config.include?(key) }
|
239
|
+
warnings.push(
|
240
|
+
msg: "Unsupported inventory configuration detected in '#{filepath}': #{transport_config.keys}. "\
|
241
|
+
"Transport configuration should be set under the 'inventory-config' option or "\
|
242
|
+
"in 'inventory.yaml'."
|
243
|
+
)
|
244
|
+
end
|
245
|
+
|
246
|
+
# Move data under transport-config to top-level so it can be easily merged with
|
247
|
+
# config from other sources.
|
248
|
+
if data.key?('inventory-config')
|
249
|
+
data = data.merge(data.delete('inventory-config'))
|
250
|
+
end
|
251
|
+
|
252
|
+
{ filepath: filepath, data: data, warnings: warnings }
|
253
|
+
end
|
254
|
+
|
255
|
+
# Loads a 'bolt.yaml' file, the legacy configuration file. There's no special munging needed
|
256
|
+
# here since Bolt::Config will just ignore any invalid keys.
|
257
|
+
def self.load_bolt_yaml(dir)
|
258
|
+
filepath = dir + BOLT_CONFIG_NAME
|
259
|
+
data = Bolt::Util.read_yaml_hash(filepath, 'config')
|
260
|
+
warnings = [msg: "Configuration file #{filepath} is deprecated and will be removed in a future version "\
|
261
|
+
"of Bolt. Use '#{dir + BOLT_DEFAULTS_NAME}' instead."]
|
262
|
+
|
263
|
+
{ filepath: filepath, data: data, warnings: warnings }
|
264
|
+
end
|
265
|
+
|
266
|
+
def self.load_defaults(project)
|
267
|
+
confs = []
|
268
|
+
|
269
|
+
# Load system-level config. Prefer a 'bolt-defaults.yaml' file, but fall back to the
|
270
|
+
# legacy 'bolt.yaml' file. If the project-level config file is also the system-level
|
271
|
+
# config file, don't load it a second time.
|
272
|
+
if File.exist?(system_path + BOLT_DEFAULTS_NAME)
|
273
|
+
confs << load_bolt_defaults_yaml(system_path)
|
274
|
+
elsif File.exist?(system_path + BOLT_CONFIG_NAME) &&
|
275
|
+
(system_path + BOLT_CONFIG_NAME) != project.config_file
|
276
|
+
confs << load_bolt_yaml(system_path)
|
277
|
+
end
|
278
|
+
|
279
|
+
# Load user-level config if there is a homedir. Prefer a 'bolt-defaults.yaml' file, but
|
280
|
+
# fall back to the legacy 'bolt.yaml' file.
|
281
|
+
if user_path
|
282
|
+
if File.exist?(user_path + BOLT_DEFAULTS_NAME)
|
283
|
+
confs << load_bolt_defaults_yaml(user_path)
|
284
|
+
elsif File.exist?(user_path + BOLT_CONFIG_NAME)
|
285
|
+
confs << load_bolt_yaml(user_path)
|
286
|
+
end
|
287
|
+
end
|
288
|
+
|
152
289
|
confs
|
153
290
|
end
|
154
291
|
|
155
292
|
def initialize(project, config_data, overrides = {})
|
156
293
|
unless config_data.is_a?(Array)
|
157
|
-
config_data = [{ filepath: project.config_file, data: config_data }]
|
294
|
+
config_data = [{ filepath: project.config_file, data: config_data, warnings: [] }]
|
158
295
|
end
|
159
296
|
|
160
|
-
@logger
|
161
|
-
@
|
162
|
-
@
|
163
|
-
@transports
|
297
|
+
@logger = Logging.logger[self]
|
298
|
+
@project = project
|
299
|
+
@warnings = @project.warnings.dup
|
300
|
+
@transports = {}
|
164
301
|
@config_files = []
|
165
302
|
|
166
303
|
default_data = {
|
@@ -178,24 +315,22 @@ module Bolt
|
|
178
315
|
'transport' => 'ssh'
|
179
316
|
}
|
180
317
|
|
181
|
-
loaded_data = config_data.
|
182
|
-
@
|
183
|
-
|
318
|
+
loaded_data = config_data.each_with_object([]) do |data, acc|
|
319
|
+
@warnings.concat(data[:warnings]) if data[:warnings].any?
|
320
|
+
|
321
|
+
if data[:data].any?
|
322
|
+
@config_files.push(data[:filepath])
|
323
|
+
acc.push(data[:data])
|
324
|
+
end
|
184
325
|
end
|
185
326
|
|
186
327
|
override_data = normalize_overrides(overrides)
|
187
328
|
|
188
329
|
# If we need to lower concurrency and concurrency is not configured
|
189
330
|
ld_concurrency = loaded_data.map(&:keys).flatten.include?('concurrency')
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
concurrency_warning = { option: 'concurrency',
|
194
|
-
msg: "Concurrency will default to #{default_concurrency} because ulimit "\
|
195
|
-
"is low: #{Etc.sysconf(Etc::SC_OPEN_MAX)}. Set concurrency with "\
|
196
|
-
"'--concurrency', or set your ulimit with 'ulimit -n <limit>'" }
|
197
|
-
@warnings << concurrency_warning
|
198
|
-
end
|
331
|
+
@modified_concurrency = default_concurrency != DEFAULT_DEFAULT_CONCURRENCY &&
|
332
|
+
!ld_concurrency &&
|
333
|
+
!override_data.key?('concurrency')
|
199
334
|
|
200
335
|
@data = merge_config_layers(default_data, *loaded_data, override_data)
|
201
336
|
|
@@ -351,7 +486,7 @@ module Bolt
|
|
351
486
|
raise Bolt::ValidationError, "Compilation is CPU-intensive, set concurrency less than #{compile_limit}"
|
352
487
|
end
|
353
488
|
|
354
|
-
|
489
|
+
if (format == 'rainbow' && Bolt::Util.windows?) || !(%w[human json rainbow].include? format)
|
355
490
|
raise Bolt::ValidationError, "Unsupported format: '#{format}'"
|
356
491
|
end
|
357
492
|
|
@@ -473,12 +608,18 @@ module Bolt
|
|
473
608
|
end.join
|
474
609
|
end
|
475
610
|
|
611
|
+
# Etc::SC_OPEN_MAX is meaningless on windows, not defined in PE Jruby and not available
|
612
|
+
# on some platforms. This method holds the logic to decide whether or not to even consider it.
|
613
|
+
def sc_open_max_available?
|
614
|
+
!Bolt::Util.windows? && defined?(Etc::SC_OPEN_MAX) && Etc.sysconf(Etc::SC_OPEN_MAX)
|
615
|
+
end
|
616
|
+
|
476
617
|
def default_concurrency
|
477
|
-
if
|
478
|
-
|
479
|
-
|
480
|
-
|
481
|
-
|
618
|
+
@default_concurrency ||= if !sc_open_max_available? || Etc.sysconf(Etc::SC_OPEN_MAX) >= 300
|
619
|
+
DEFAULT_DEFAULT_CONCURRENCY
|
620
|
+
else
|
621
|
+
Etc.sysconf(Etc::SC_OPEN_MAX) / 7
|
622
|
+
end
|
482
623
|
end
|
483
624
|
end
|
484
625
|
end
|