bolt 3.14.1 → 3.17.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/functions/apply_prep.rb +137 -104
- data/guides/debugging.yaml +27 -0
- data/guides/inventory.yaml +23 -0
- data/guides/links.yaml +12 -0
- data/guides/logging.yaml +17 -0
- data/guides/module.yaml +18 -0
- data/guides/modulepath.yaml +24 -0
- data/guides/project.yaml +21 -0
- data/guides/targets.yaml +28 -0
- data/guides/transports.yaml +22 -0
- data/lib/bolt/analytics.rb +2 -19
- data/lib/bolt/application.rb +634 -0
- data/lib/bolt/bolt_option_parser.rb +28 -4
- data/lib/bolt/cli.rb +592 -788
- data/lib/bolt/fiber_executor.rb +7 -3
- data/lib/bolt/inventory/inventory.rb +68 -39
- data/lib/bolt/inventory.rb +2 -9
- data/lib/bolt/module_installer/puppetfile.rb +24 -10
- data/lib/bolt/outputter/human.rb +83 -32
- data/lib/bolt/outputter/json.rb +63 -38
- data/lib/bolt/pal/yaml_plan/transpiler.rb +1 -1
- data/lib/bolt/pal.rb +31 -11
- data/lib/bolt/plan_creator.rb +84 -25
- data/lib/bolt/plan_future.rb +11 -6
- data/lib/bolt/plan_result.rb +1 -1
- data/lib/bolt/plugin/task.rb +1 -1
- data/lib/bolt/plugin.rb +11 -17
- data/lib/bolt/project.rb +0 -7
- data/lib/bolt/result_set.rb +2 -1
- data/lib/bolt/transport/local/connection.rb +17 -1
- data/lib/bolt/transport/orch/connection.rb +13 -1
- data/lib/bolt/version.rb +1 -1
- data/lib/bolt_server/file_cache.rb +12 -0
- data/lib/bolt_server/schemas/action-apply.json +32 -0
- data/lib/bolt_server/schemas/action-apply_prep.json +19 -0
- data/lib/bolt_server/transport_app.rb +113 -24
- data/lib/bolt_spec/bolt_context.rb +1 -1
- data/lib/bolt_spec/run.rb +1 -1
- metadata +14 -3
- data/lib/bolt/secret.rb +0 -37
@@ -897,7 +897,7 @@ module Bolt
|
|
897
897
|
end
|
898
898
|
define('--params PARAMETERS',
|
899
899
|
"Parameters to a task or plan as json, a json file '@<file>', or on stdin '-'.") do |params|
|
900
|
-
@options[:
|
900
|
+
@options[:params] = parse_params(params)
|
901
901
|
end
|
902
902
|
define('-e', '--execute CODE',
|
903
903
|
"Puppet manifest code to apply to the targets.") do |code|
|
@@ -1042,6 +1042,21 @@ module Bolt
|
|
1042
1042
|
define('--pp', 'Create a new Puppet language plan.') do |_|
|
1043
1043
|
@options[:puppet] = true
|
1044
1044
|
end
|
1045
|
+
define('--script SCRIPT', 'Create a new plan that wraps a script.') do |path|
|
1046
|
+
# If the path is a relative, absolute, or not a scripts path, raise an
|
1047
|
+
# error. This flag is intended to be used to create shareable plans.
|
1048
|
+
#
|
1049
|
+
# This also limits valid mounts to files and scripts, which we may want
|
1050
|
+
# to expand in the future.
|
1051
|
+
if File.exist?(path) || Pathname.new(path).absolute? ||
|
1052
|
+
!%w[scripts files].include?(path.split(File::SEPARATOR)[1])
|
1053
|
+
raise Bolt::CLIError, "The script must be a detailed Puppet file reference, " \
|
1054
|
+
"for example 'mymodule/scripts/myscript.sh'. See http://pup.pt/bolt-scripts for " \
|
1055
|
+
"more information on detailed Puppet file references."
|
1056
|
+
end
|
1057
|
+
|
1058
|
+
@options[:plan_script] = path
|
1059
|
+
end
|
1045
1060
|
|
1046
1061
|
separator "\n#{self.class.colorize(:cyan, 'Display options')}"
|
1047
1062
|
define('--filter FILTER', 'Filter tasks and plans by a matching substring.') do |filter|
|
@@ -1086,8 +1101,7 @@ module Bolt
|
|
1086
1101
|
@options[:help] = true
|
1087
1102
|
end
|
1088
1103
|
define('--version', 'Display the version.') do |_|
|
1089
|
-
|
1090
|
-
raise Bolt::CLIExit
|
1104
|
+
@options[:version] = true
|
1091
1105
|
end
|
1092
1106
|
define('--log-level LEVEL',
|
1093
1107
|
"Set the log level for the console. Available options are",
|
@@ -1095,7 +1109,7 @@ module Bolt
|
|
1095
1109
|
@options[:log] = { 'console' => { 'level' => level } }
|
1096
1110
|
end
|
1097
1111
|
define('--clear-cache',
|
1098
|
-
"Clear plugin
|
1112
|
+
"Clear plugin, plan, and task caches before executing.") do |_|
|
1099
1113
|
@options[:clear_cache] = true
|
1100
1114
|
end
|
1101
1115
|
define('--plugin PLUGIN', 'Select the plugin to use.') do |plug|
|
@@ -1130,5 +1144,15 @@ module Bolt
|
|
1130
1144
|
rescue JSON::ParserError => e
|
1131
1145
|
raise Bolt::CLIError, "Unable to parse --params value as JSON: #{e}"
|
1132
1146
|
end
|
1147
|
+
|
1148
|
+
def permute(args)
|
1149
|
+
super(args)
|
1150
|
+
rescue OptionParser::MissingArgument => e
|
1151
|
+
raise Bolt::CLIError, "Option '#{e.args.first}' needs a parameter"
|
1152
|
+
rescue OptionParser::InvalidArgument => e
|
1153
|
+
raise Bolt::CLIError, "Invalid parameter specified for option '#{e.args.first}': #{e.args[1]}"
|
1154
|
+
rescue OptionParser::InvalidOption, OptionParser::AmbiguousOption => e
|
1155
|
+
raise Bolt::CLIError, "Unknown argument '#{e.args.first}'"
|
1156
|
+
end
|
1133
1157
|
end
|
1134
1158
|
end
|
data/lib/bolt/cli.rb
CHANGED
@@ -10,6 +10,7 @@ require 'io/console'
|
|
10
10
|
require 'logging'
|
11
11
|
require 'optparse'
|
12
12
|
require 'bolt/analytics'
|
13
|
+
require 'bolt/application'
|
13
14
|
require 'bolt/bolt_option_parser'
|
14
15
|
require 'bolt/config'
|
15
16
|
require 'bolt/error'
|
@@ -19,12 +20,10 @@ require 'bolt/logger'
|
|
19
20
|
require 'bolt/module_installer'
|
20
21
|
require 'bolt/outputter'
|
21
22
|
require 'bolt/pal'
|
22
|
-
require 'bolt/plan_creator'
|
23
23
|
require 'bolt/plugin'
|
24
24
|
require 'bolt/project_manager'
|
25
25
|
require 'bolt/puppetdb'
|
26
26
|
require 'bolt/rerun'
|
27
|
-
require 'bolt/secret'
|
28
27
|
require 'bolt/target'
|
29
28
|
require 'bolt/version'
|
30
29
|
|
@@ -32,6 +31,8 @@ module Bolt
|
|
32
31
|
class CLIExit < StandardError; end
|
33
32
|
|
34
33
|
class CLI
|
34
|
+
attr_reader :outputter, :rerun
|
35
|
+
|
35
36
|
COMMANDS = {
|
36
37
|
'apply' => %w[],
|
37
38
|
'command' => %w[run],
|
@@ -51,22 +52,24 @@ module Bolt
|
|
51
52
|
|
52
53
|
TARGETING_OPTIONS = %i[query rerun targets].freeze
|
53
54
|
|
54
|
-
|
55
|
+
SUCCESS = 0
|
56
|
+
ERROR = 1
|
57
|
+
FAILURE = 2
|
55
58
|
|
56
59
|
def initialize(argv)
|
57
60
|
Bolt::Logger.initialize_logging
|
58
61
|
@logger = Bolt::Logger.logger(self)
|
59
|
-
@argv
|
60
|
-
@options = {}
|
61
|
-
end
|
62
|
-
|
63
|
-
# Only call after @config has been initialized.
|
64
|
-
def inventory
|
65
|
-
@inventory ||= Bolt::Inventory.from_config(config, plugins)
|
62
|
+
@argv = argv
|
66
63
|
end
|
67
|
-
private :inventory
|
68
64
|
|
69
|
-
|
65
|
+
# TODO: Move this to the parser.
|
66
|
+
#
|
67
|
+
# Query whether the help text needs to be displayed.
|
68
|
+
#
|
69
|
+
# @param remaining [Array] Remaining arguments after parsing the command.
|
70
|
+
# @param options [Hash] The CLI options.
|
71
|
+
#
|
72
|
+
private def help?(options, remaining)
|
70
73
|
# Set the subcommand
|
71
74
|
options[:subcommand] = remaining.shift
|
72
75
|
|
@@ -84,24 +87,90 @@ module Bolt
|
|
84
87
|
|
85
88
|
options[:help]
|
86
89
|
end
|
87
|
-
private :help?
|
88
90
|
|
89
|
-
#
|
90
|
-
#
|
91
|
-
#
|
91
|
+
# TODO: Move most of this to the parser.
|
92
|
+
#
|
93
|
+
# Parse the command and validate options. All errors that are raised here
|
94
|
+
# are not handled by the outputter, as it relies on config being loaded.
|
92
95
|
#
|
93
|
-
# This separation is needed since the Bolt::Outputter class that normally handles
|
94
|
-
# printing errors relies on config being loaded. All setup that happens before
|
95
|
-
# config is loaded will have errors printed directly to stdout, while all errors
|
96
|
-
# raised after config is loaded are handled by the outputter.
|
97
96
|
def parse
|
98
|
-
|
99
|
-
|
100
|
-
|
97
|
+
with_error_handling do
|
98
|
+
options = {}
|
99
|
+
parser = BoltOptionParser.new(options)
|
100
|
+
|
101
|
+
# This part aims to handle both `bolt <mode> --help` and `bolt help <mode>`.
|
102
|
+
remaining = parser.permute(@argv) unless @argv.empty?
|
103
|
+
|
104
|
+
if @argv.empty? || help?(options, remaining)
|
105
|
+
# If the subcommand is not enabled, display the default
|
106
|
+
# help text
|
107
|
+
options[:subcommand] = nil unless COMMANDS.include?(options[:subcommand])
|
108
|
+
|
109
|
+
if Bolt::Util.first_run?
|
110
|
+
FileUtils.touch(Bolt::Util.first_runs_free)
|
111
|
+
|
112
|
+
if options[:subcommand].nil? && $stdout.isatty
|
113
|
+
welcome_message
|
114
|
+
raise Bolt::CLIExit
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
# Update the parser for the subcommand (or lack thereof)
|
119
|
+
parser.update
|
120
|
+
puts parser.help
|
121
|
+
raise Bolt::CLIExit
|
122
|
+
end
|
123
|
+
|
124
|
+
if options[:version]
|
125
|
+
puts Bolt::VERSION
|
126
|
+
raise Bolt::CLIExit
|
127
|
+
end
|
128
|
+
|
129
|
+
options[:object] = remaining.shift
|
130
|
+
|
131
|
+
# Handle reading a command from a file
|
132
|
+
if options[:subcommand] == 'command' && options[:object]
|
133
|
+
options[:object] = Bolt::Util.get_arg_input(options[:object])
|
134
|
+
end
|
135
|
+
|
136
|
+
# Only parse params for task or plan
|
137
|
+
if %w[task plan].include?(options[:subcommand])
|
138
|
+
params, remaining = remaining.partition { |s| s =~ /.+=/ }
|
139
|
+
if options[:params]
|
140
|
+
unless params.empty?
|
141
|
+
raise Bolt::CLIError,
|
142
|
+
"Parameters must be specified through either the --params " \
|
143
|
+
"option or param=value pairs, not both"
|
144
|
+
end
|
145
|
+
options[:params_parsed] = true
|
146
|
+
elsif params.any?
|
147
|
+
options[:params_parsed] = false
|
148
|
+
options[:params] = Hash[params.map { |a| a.split('=', 2) }]
|
149
|
+
else
|
150
|
+
options[:params_parsed] = true
|
151
|
+
options[:params] = {}
|
152
|
+
end
|
153
|
+
end
|
154
|
+
options[:leftovers] = remaining
|
155
|
+
|
156
|
+
# Default to verbose for everything except plans
|
157
|
+
unless options.key?(:verbose)
|
158
|
+
options[:verbose] = options[:subcommand] != 'plan'
|
159
|
+
end
|
160
|
+
|
161
|
+
validate(options)
|
162
|
+
validate_ps_version
|
163
|
+
|
164
|
+
options
|
165
|
+
end
|
101
166
|
end
|
102
167
|
|
103
|
-
#
|
104
|
-
|
168
|
+
# TODO: Move this to the parser.
|
169
|
+
#
|
170
|
+
# Print a welcome message when users first install Bolt and run `bolt`,
|
171
|
+
# `bolt help` or `bolt --help`.
|
172
|
+
#
|
173
|
+
private def welcome_message
|
105
174
|
bolt = <<~BOLT
|
106
175
|
`.::-`
|
107
176
|
`.-:///////-.`
|
@@ -142,149 +211,15 @@ module Bolt
|
|
142
211
|
$stdout.print message
|
143
212
|
end
|
144
213
|
|
145
|
-
#
|
146
|
-
#
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
options[:subcommand] = nil unless COMMANDS.include?(options[:subcommand])
|
155
|
-
|
156
|
-
if Bolt::Util.first_run?
|
157
|
-
FileUtils.touch(Bolt::Util.first_runs_free)
|
158
|
-
|
159
|
-
if options[:subcommand].nil? && $stdout.isatty
|
160
|
-
welcome_message
|
161
|
-
raise Bolt::CLIExit
|
162
|
-
end
|
163
|
-
end
|
164
|
-
|
165
|
-
# Update the parser for the subcommand (or lack thereof)
|
166
|
-
parser.update
|
167
|
-
puts parser.help
|
168
|
-
raise Bolt::CLIExit
|
169
|
-
end
|
170
|
-
|
171
|
-
options[:object] = remaining.shift
|
172
|
-
|
173
|
-
# Handle reading a command from a file
|
174
|
-
if options[:subcommand] == 'command' && options[:object]
|
175
|
-
options[:object] = Bolt::Util.get_arg_input(options[:object])
|
176
|
-
end
|
177
|
-
|
178
|
-
# Only parse task_options for task or plan
|
179
|
-
if %w[task plan].include?(options[:subcommand])
|
180
|
-
task_options, remaining = remaining.partition { |s| s =~ /.+=/ }
|
181
|
-
if options[:task_options]
|
182
|
-
unless task_options.empty?
|
183
|
-
raise Bolt::CLIError,
|
184
|
-
"Parameters must be specified through either the --params " \
|
185
|
-
"option or param=value pairs, not both"
|
186
|
-
end
|
187
|
-
options[:params_parsed] = true
|
188
|
-
elsif task_options.any?
|
189
|
-
options[:params_parsed] = false
|
190
|
-
options[:task_options] = Hash[task_options.map { |a| a.split('=', 2) }]
|
191
|
-
else
|
192
|
-
options[:params_parsed] = true
|
193
|
-
options[:task_options] = {}
|
194
|
-
end
|
195
|
-
end
|
196
|
-
options[:leftovers] = remaining
|
197
|
-
|
198
|
-
# Default to verbose for everything except plans
|
199
|
-
unless options.key?(:verbose)
|
200
|
-
options[:verbose] = options[:subcommand] != 'plan'
|
201
|
-
end
|
202
|
-
|
203
|
-
validate(options)
|
204
|
-
rescue Bolt::Error => e
|
205
|
-
fatal_error(e)
|
206
|
-
raise e
|
207
|
-
end
|
208
|
-
|
209
|
-
# Loads the project and configuration. All errors that are raised here are not
|
210
|
-
# handled by the outputter, as it relies on config being loaded.
|
211
|
-
def load_config
|
212
|
-
project = if ENV['BOLT_PROJECT']
|
213
|
-
Bolt::Project.create_project(ENV['BOLT_PROJECT'], 'environment')
|
214
|
-
elsif options[:project]
|
215
|
-
dir = Pathname.new(options[:project])
|
216
|
-
if (dir + Bolt::Project::BOLTDIR_NAME).directory?
|
217
|
-
Bolt::Project.create_project(dir + Bolt::Project::BOLTDIR_NAME)
|
218
|
-
else
|
219
|
-
Bolt::Project.create_project(dir)
|
220
|
-
end
|
221
|
-
else
|
222
|
-
Bolt::Project.find_boltdir(Dir.pwd)
|
223
|
-
end
|
224
|
-
@config = Bolt::Config.from_project(project, options)
|
225
|
-
rescue Bolt::Error => e
|
226
|
-
fatal_error(e)
|
227
|
-
raise e
|
228
|
-
end
|
229
|
-
|
230
|
-
# Completes the setup process by configuring Bolt and log messages
|
231
|
-
def finalize_setup
|
232
|
-
Bolt::Logger.configure(config.log, config.color, config.disable_warnings)
|
233
|
-
Bolt::Logger.stream = config.stream
|
234
|
-
Bolt::Logger.analytics = analytics
|
235
|
-
Bolt::Logger.flush_queue
|
236
|
-
|
237
|
-
# Logger must be configured before checking path case and project file, otherwise logs will not display
|
238
|
-
config.check_path_case('modulepath', config.modulepath)
|
239
|
-
config.project.check_deprecated_file
|
240
|
-
|
241
|
-
if options[:clear_cache] && File.exist?(config.project.plugin_cache_file)
|
242
|
-
FileUtils.rm(config.project.plugin_cache_file)
|
243
|
-
end
|
244
|
-
|
245
|
-
warn_inventory_overrides_cli(options)
|
246
|
-
validate_ps_version
|
247
|
-
|
248
|
-
options
|
249
|
-
rescue Bolt::Error => e
|
250
|
-
outputter.fatal_error(e)
|
251
|
-
raise e
|
252
|
-
end
|
253
|
-
|
254
|
-
private def validate_ps_version
|
255
|
-
if Bolt::Util.powershell?
|
256
|
-
command = "powershell.exe -NoProfile -NonInteractive -NoLogo -ExecutionPolicy "\
|
257
|
-
"Bypass -Command $PSVersionTable.PSVersion.Major"
|
258
|
-
stdout, _stderr, _status = Open3.capture3(command)
|
259
|
-
|
260
|
-
return unless !stdout.empty? && stdout.to_i < 3
|
261
|
-
|
262
|
-
msg = "Detected PowerShell 2 on controller. PowerShell 2 is unsupported."
|
263
|
-
Bolt::Logger.deprecation_warning("powershell_2_controller", msg)
|
264
|
-
end
|
265
|
-
end
|
266
|
-
|
267
|
-
def update_targets(options)
|
268
|
-
target_opts = options.keys.select { |opt| TARGETING_OPTIONS.include?(opt) }
|
269
|
-
target_string = "'--targets', '--rerun', or '--query'"
|
270
|
-
if target_opts.length > 1
|
271
|
-
raise Bolt::CLIError, "Only one targeting option #{target_string} can be specified"
|
272
|
-
elsif target_opts.empty? && options[:subcommand] != 'plan'
|
273
|
-
raise Bolt::CLIError, "Command requires a targeting option: #{target_string}"
|
274
|
-
end
|
275
|
-
|
276
|
-
targets = if options[:query]
|
277
|
-
query_puppetdb_nodes(options[:query])
|
278
|
-
elsif options[:rerun]
|
279
|
-
rerun.get_targets(options[:rerun])
|
280
|
-
else
|
281
|
-
options[:targets] || []
|
282
|
-
end
|
283
|
-
options[:target_args] = targets
|
284
|
-
options[:targets] = inventory.get_targets(targets)
|
285
|
-
end
|
286
|
-
|
287
|
-
def validate(options)
|
214
|
+
# TODO: Move this to the parser.
|
215
|
+
#
|
216
|
+
# Validate the command. Ensure that the subcommand and action are
|
217
|
+
# recognized, all required arguments are specified, and only supported
|
218
|
+
# command-line options are used.
|
219
|
+
#
|
220
|
+
# @param options [Hash] The CLI options.
|
221
|
+
#
|
222
|
+
private def validate(options)
|
288
223
|
unless COMMANDS.include?(options[:subcommand])
|
289
224
|
command = Bolt::Util.powershell? ? 'Get-Command -Module PuppetBolt' : 'bolt help'
|
290
225
|
raise Bolt::CLIError,
|
@@ -349,12 +284,34 @@ module Bolt
|
|
349
284
|
raise Bolt::CLIError, "Must specify a module name."
|
350
285
|
end
|
351
286
|
|
287
|
+
if options[:action] == 'convert' && !options[:object]
|
288
|
+
raise Bolt::CLIError, "Must specify a plan."
|
289
|
+
end
|
290
|
+
|
352
291
|
if options[:subcommand] == 'module' && options[:action] == 'install' && options[:object]
|
353
292
|
command = Bolt::Util.powershell? ? 'Add-BoltModule -Module' : 'bolt module add'
|
354
293
|
raise Bolt::CLIError, "Invalid argument '#{options[:object]}'. To add a new module to "\
|
355
294
|
"the project, run '#{command} #{options[:object]}'."
|
356
295
|
end
|
357
296
|
|
297
|
+
if %w[download upload].include?(options[:action])
|
298
|
+
raise Bolt::CLIError, "Must specify a source" unless options[:object]
|
299
|
+
|
300
|
+
if options[:leftovers].empty?
|
301
|
+
raise Bolt::CLIError, "Must specify a destination"
|
302
|
+
elsif options[:leftovers].size > 1
|
303
|
+
raise Bolt::CLIError, "Unknown arguments #{options[:leftovers].drop(1).join(', ')}"
|
304
|
+
end
|
305
|
+
end
|
306
|
+
|
307
|
+
if options[:subcommand] == 'group' && options[:object]
|
308
|
+
raise Bolt::CLIError, "Unknown argument #{options[:object]}"
|
309
|
+
end
|
310
|
+
|
311
|
+
if options[:action] == 'generate-types' && options[:object]
|
312
|
+
raise Bolt::CLIError, "Unknown argument #{options[:object]}"
|
313
|
+
end
|
314
|
+
|
358
315
|
if !%w[file script lookup].include?(options[:subcommand]) &&
|
359
316
|
!options[:leftovers].empty?
|
360
317
|
raise Bolt::CLIError,
|
@@ -379,725 +336,572 @@ module Bolt
|
|
379
336
|
"Option '--env-var' can only be specified when running a command or script"
|
380
337
|
end
|
381
338
|
end
|
382
|
-
end
|
383
|
-
|
384
|
-
def handle_parser_errors
|
385
|
-
yield
|
386
|
-
rescue OptionParser::MissingArgument => e
|
387
|
-
raise Bolt::CLIError, "Option '#{e.args.first}' needs a parameter"
|
388
|
-
rescue OptionParser::InvalidArgument => e
|
389
|
-
raise Bolt::CLIError, "Invalid parameter specified for option '#{e.args.first}': #{e.args[1]}"
|
390
|
-
rescue OptionParser::InvalidOption, OptionParser::AmbiguousOption => e
|
391
|
-
raise Bolt::CLIError, "Unknown argument '#{e.args.first}'"
|
392
|
-
end
|
393
339
|
|
394
|
-
|
395
|
-
plugins.puppetdb_client
|
340
|
+
validate_targeting_options(options)
|
396
341
|
end
|
397
342
|
|
398
|
-
|
399
|
-
|
400
|
-
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
def warn_inventory_overrides_cli(opts)
|
407
|
-
inventory_source = if ENV[Bolt::Inventory::ENVIRONMENT_VAR]
|
408
|
-
Bolt::Inventory::ENVIRONMENT_VAR
|
409
|
-
elsif config.inventoryfile
|
410
|
-
config.inventoryfile
|
411
|
-
elsif File.exist?(config.default_inventoryfile)
|
412
|
-
config.default_inventoryfile
|
413
|
-
end
|
343
|
+
# Validates that only one targeting option is provided and that commands
|
344
|
+
# requiring a targeting option received one.
|
345
|
+
#
|
346
|
+
# @param options [Hash] The CLI options.
|
347
|
+
#
|
348
|
+
private def validate_targeting_options(options)
|
349
|
+
target_opts = options.slice(*TARGETING_OPTIONS)
|
350
|
+
target_string = "'--targets', '--rerun', or '--query'"
|
414
351
|
|
415
|
-
|
416
|
-
|
352
|
+
if target_opts.length > 1
|
353
|
+
raise Bolt::CLIError, "Only one targeting option can be specified: #{target_string}"
|
417
354
|
end
|
418
355
|
|
419
|
-
|
356
|
+
return if %w[guide module plan project secret].include?(options[:subcommand]) ||
|
357
|
+
%w[convert new show].include?(options[:action]) ||
|
358
|
+
options[:plan_hierarchy]
|
420
359
|
|
421
|
-
|
422
|
-
|
423
|
-
if inventory_source && conflicting_options.any?
|
424
|
-
Bolt::Logger.warn(
|
425
|
-
"cli_overrides",
|
426
|
-
"CLI arguments #{conflicting_options.to_a} might be overridden by Inventory: #{inventory_source}"
|
427
|
-
)
|
360
|
+
if target_opts.empty?
|
361
|
+
raise Bolt::CLIError, "Command requires a targeting option: #{target_string}"
|
428
362
|
end
|
429
363
|
end
|
430
364
|
|
365
|
+
# Execute a Bolt command. The +options+ hash includes the subcommand and
|
366
|
+
# action to be run, as well as any additional arguments and options for the
|
367
|
+
# command.
|
368
|
+
#
|
369
|
+
# @param options [Hash] The CLI options.
|
370
|
+
#
|
431
371
|
def execute(options)
|
432
|
-
|
433
|
-
|
434
|
-
|
435
|
-
|
436
|
-
|
437
|
-
|
438
|
-
|
439
|
-
|
440
|
-
|
441
|
-
|
442
|
-
|
443
|
-
|
444
|
-
|
445
|
-
|
446
|
-
|
447
|
-
|
448
|
-
|
449
|
-
|
450
|
-
|
451
|
-
|
452
|
-
|
453
|
-
|
454
|
-
end
|
372
|
+
with_signal_handling do
|
373
|
+
with_error_handling do
|
374
|
+
# TODO: Separate from options hash and pass as own args.
|
375
|
+
command = options[:subcommand]
|
376
|
+
action = options[:action]
|
377
|
+
|
378
|
+
#
|
379
|
+
# INITIALIZE CORE CLASSES
|
380
|
+
#
|
381
|
+
|
382
|
+
project = if ENV['BOLT_PROJECT']
|
383
|
+
Bolt::Project.create_project(ENV['BOLT_PROJECT'], 'environment')
|
384
|
+
elsif options[:project]
|
385
|
+
dir = Pathname.new(options[:project])
|
386
|
+
if (dir + Bolt::Project::BOLTDIR_NAME).directory?
|
387
|
+
Bolt::Project.create_project(dir + Bolt::Project::BOLTDIR_NAME)
|
388
|
+
else
|
389
|
+
Bolt::Project.create_project(dir)
|
390
|
+
end
|
391
|
+
else
|
392
|
+
Bolt::Project.find_boltdir(Dir.pwd)
|
393
|
+
end
|
455
394
|
|
456
|
-
|
457
|
-
output_format: config.format,
|
458
|
-
# For continuity
|
459
|
-
boltdir_type: config.project.type
|
460
|
-
}.merge!(analytics.plan_counts(config.project.plans_path))
|
395
|
+
config = Bolt::Config.from_project(project, options)
|
461
396
|
|
462
|
-
|
463
|
-
|
464
|
-
|
465
|
-
|
466
|
-
|
467
|
-
|
468
|
-
|
469
|
-
end
|
397
|
+
@outputter = Bolt::Outputter.for_format(
|
398
|
+
config.format,
|
399
|
+
config.color,
|
400
|
+
options[:verbose],
|
401
|
+
config.trace,
|
402
|
+
config.spinner
|
403
|
+
)
|
470
404
|
|
471
|
-
|
405
|
+
@rerun = Bolt::Rerun.new(config.rerunfile, config.save_rerun)
|
472
406
|
|
473
|
-
|
474
|
-
|
475
|
-
|
476
|
-
|
477
|
-
|
478
|
-
show_task(options[:object])
|
479
|
-
else
|
480
|
-
list_tasks
|
407
|
+
# TODO: Subscribe this to the executor.
|
408
|
+
analytics = begin
|
409
|
+
client = Bolt::Analytics.build_client(config.analytics)
|
410
|
+
client.bundled_content = bundled_content(options)
|
411
|
+
client
|
481
412
|
end
|
482
|
-
|
483
|
-
|
484
|
-
|
485
|
-
|
486
|
-
|
413
|
+
|
414
|
+
Bolt::Logger.configure(config.log, config.color, config.disable_warnings)
|
415
|
+
Bolt::Logger.stream = config.stream
|
416
|
+
Bolt::Logger.analytics = analytics
|
417
|
+
Bolt::Logger.flush_queue
|
418
|
+
|
419
|
+
executor = Bolt::Executor.new(
|
420
|
+
config.concurrency,
|
421
|
+
analytics,
|
422
|
+
options[:noop],
|
423
|
+
config.modified_concurrency,
|
424
|
+
config.future
|
425
|
+
)
|
426
|
+
|
427
|
+
pal = Bolt::PAL.new(
|
428
|
+
Bolt::Config::Modulepath.new(config.modulepath),
|
429
|
+
config.hiera_config,
|
430
|
+
config.project.resource_types,
|
431
|
+
config.compile_concurrency,
|
432
|
+
config.trusted_external,
|
433
|
+
config.apply_settings,
|
434
|
+
config.project
|
435
|
+
)
|
436
|
+
|
437
|
+
plugins = Bolt::Plugin.new(config, pal, analytics)
|
438
|
+
|
439
|
+
inventory = Bolt::Inventory.from_config(config, plugins)
|
440
|
+
|
441
|
+
log_outputter = Bolt::Outputter::Logger.new(options[:verbose], config.trace)
|
442
|
+
|
443
|
+
#
|
444
|
+
# FINALIZING SETUP
|
445
|
+
#
|
446
|
+
|
447
|
+
check_gem_install
|
448
|
+
warn_inventory_overrides_cli(config, options)
|
449
|
+
submit_screen_view(analytics, config, inventory, options)
|
450
|
+
options[:targets] = process_target_list(plugins.puppetdb_client, @rerun, options)
|
451
|
+
|
452
|
+
# TODO: Fix casing issue in Windows.
|
453
|
+
config.check_path_case('modulepath', config.modulepath)
|
454
|
+
|
455
|
+
if options[:clear_cache]
|
456
|
+
FileUtils.rm(config.project.plugin_cache_file) if File.exist?(config.project.plugin_cache_file)
|
457
|
+
FileUtils.rm(config.project.task_cache_file) if File.exist?(config.project.task_cache_file)
|
458
|
+
FileUtils.rm(config.project.plan_cache_file) if File.exist?(config.project.plan_cache_file)
|
487
459
|
end
|
488
|
-
|
489
|
-
|
490
|
-
|
460
|
+
|
461
|
+
case command
|
462
|
+
when 'apply', 'lookup'
|
463
|
+
if %w[human rainbow].include?(config.format)
|
464
|
+
executor.subscribe(outputter)
|
465
|
+
end
|
466
|
+
when 'plan'
|
467
|
+
if %w[human rainbow].include?(config.format)
|
468
|
+
executor.subscribe(outputter)
|
469
|
+
else
|
470
|
+
executor.subscribe(outputter, %i[message verbose])
|
471
|
+
end
|
491
472
|
else
|
492
|
-
|
473
|
+
executor.subscribe(outputter)
|
493
474
|
end
|
494
|
-
|
495
|
-
|
496
|
-
|
497
|
-
|
498
|
-
|
499
|
-
|
500
|
-
|
475
|
+
|
476
|
+
executor.subscribe(log_outputter)
|
477
|
+
|
478
|
+
# TODO: Figure out where this should really go. It doesn't seem to
|
479
|
+
# make sense in the application, since the params should already
|
480
|
+
# be data when they reach that point.
|
481
|
+
if %w[plan task].include?(command) && action == 'run'
|
482
|
+
options[:params] = parse_params(
|
483
|
+
command,
|
484
|
+
options[:object],
|
485
|
+
pal,
|
486
|
+
**options.slice(:params, :params_parsed)
|
487
|
+
)
|
501
488
|
end
|
502
|
-
|
503
|
-
|
489
|
+
|
490
|
+
application = Bolt::Application.new(
|
491
|
+
analytics: analytics,
|
492
|
+
config: config,
|
493
|
+
executor: executor,
|
494
|
+
inventory: inventory,
|
495
|
+
pal: pal,
|
496
|
+
plugins: plugins
|
497
|
+
)
|
498
|
+
|
499
|
+
process_command(application, command, action, options)
|
500
|
+
ensure
|
501
|
+
analytics&.finish
|
504
502
|
end
|
505
|
-
return 0
|
506
|
-
when 'convert'
|
507
|
-
pal.convert_plan(options[:object])
|
508
|
-
return 0
|
509
503
|
end
|
504
|
+
end
|
505
|
+
|
506
|
+
# Process the command.
|
507
|
+
#
|
508
|
+
# @param app [Bolt::Application] The application.
|
509
|
+
# @param command [String] The command.
|
510
|
+
# @param action [String, NilClass] The action.
|
511
|
+
# @param options [Hash] The CLI options.
|
512
|
+
#
|
513
|
+
private def process_command(app, command, action, options)
|
514
|
+
case command
|
515
|
+
when 'apply'
|
516
|
+
results = outputter.spin do
|
517
|
+
app.apply(options[:object], options[:targets], **options.slice(:code, :noop))
|
518
|
+
end
|
519
|
+
rerun.update(results)
|
520
|
+
app.shutdown
|
521
|
+
outputter.print_apply_result(results)
|
522
|
+
results.ok? ? SUCCESS : FAILURE
|
510
523
|
|
511
|
-
|
524
|
+
when 'command'
|
525
|
+
outputter.print_head
|
526
|
+
results = app.run_command(options[:object], options[:targets], **options.slice(:env_vars))
|
527
|
+
rerun.update(results)
|
528
|
+
app.shutdown
|
529
|
+
outputter.print_summary(results, results.elapsed_time)
|
530
|
+
results.ok? ? SUCCESS : FAILURE
|
531
|
+
|
532
|
+
when 'file'
|
533
|
+
case action
|
534
|
+
when 'download'
|
535
|
+
outputter.print_head
|
536
|
+
results = app.download_file(options[:object], options[:leftovers].first, options[:targets])
|
537
|
+
rerun.update(results)
|
538
|
+
app.shutdown
|
539
|
+
outputter.print_summary(results, results.elapsed_time)
|
540
|
+
results.ok? ? SUCCESS : FAILURE
|
541
|
+
when 'upload'
|
542
|
+
outputter.print_head
|
543
|
+
results = app.upload_file(options[:object], options[:leftovers].first, options[:targets])
|
544
|
+
rerun.update(results)
|
545
|
+
app.shutdown
|
546
|
+
outputter.print_summary(results, results.elapsed_time)
|
547
|
+
results.ok? ? SUCCESS : FAILURE
|
548
|
+
end
|
512
549
|
|
513
|
-
|
514
|
-
|
515
|
-
|
550
|
+
when 'group'
|
551
|
+
outputter.print_groups(**app.list_groups)
|
552
|
+
SUCCESS
|
516
553
|
|
517
|
-
case options[:subcommand]
|
518
554
|
when 'guide'
|
519
|
-
|
520
|
-
|
521
|
-
|
522
|
-
|
523
|
-
end
|
524
|
-
when 'project'
|
525
|
-
case options[:action]
|
526
|
-
when 'init'
|
527
|
-
code = Bolt::ProjectManager.new(config, outputter, pal)
|
528
|
-
.create(Dir.pwd, options[:object], options[:modules])
|
529
|
-
when 'migrate'
|
530
|
-
code = Bolt::ProjectManager.new(config, outputter, pal).migrate
|
555
|
+
if options[:object]
|
556
|
+
outputter.print_guide(**app.show_guide(options[:object]))
|
557
|
+
else
|
558
|
+
outputter.print_topics(**app.list_guides)
|
531
559
|
end
|
532
|
-
|
533
|
-
|
534
|
-
|
535
|
-
|
536
|
-
|
537
|
-
|
538
|
-
|
560
|
+
SUCCESS
|
561
|
+
|
562
|
+
when 'inventory'
|
563
|
+
targets = app.show_inventory(options[:targets])
|
564
|
+
.merge(flag: !options[:targets].nil?)
|
565
|
+
if options[:detail]
|
566
|
+
outputter.print_target_info(**targets)
|
567
|
+
else
|
568
|
+
outputter.print_targets(**targets)
|
539
569
|
end
|
540
|
-
|
541
|
-
|
542
|
-
|
543
|
-
|
544
|
-
|
545
|
-
|
546
|
-
|
547
|
-
|
548
|
-
|
549
|
-
|
550
|
-
|
551
|
-
|
552
|
-
|
570
|
+
SUCCESS
|
571
|
+
|
572
|
+
when 'lookup'
|
573
|
+
options[:vars] = parse_vars(options[:leftovers])
|
574
|
+
if options[:plan_hierarchy]
|
575
|
+
outputter.print_plan_lookup(app.plan_lookup(options[:object], **options.slice(:vars)))
|
576
|
+
SUCCESS
|
577
|
+
else
|
578
|
+
results = outputter.spin do
|
579
|
+
app.lookup(options[:object], options[:targets], **options.slice(:vars))
|
580
|
+
end
|
581
|
+
rerun.update(results)
|
582
|
+
app.shutdown
|
583
|
+
outputter.print_result_set(results)
|
584
|
+
results.ok? ? SUCCESS : FAILURE
|
553
585
|
end
|
586
|
+
|
554
587
|
when 'module'
|
555
|
-
case
|
588
|
+
case action
|
556
589
|
when 'add'
|
557
|
-
|
558
|
-
|
559
|
-
code = install_project_modules(config.project, config.module_install, options[:force], options[:resolve])
|
590
|
+
ok = outputter.spin { app.add_module(options[:object], outputter) }
|
591
|
+
ok ? SUCCESS : FAILURE
|
560
592
|
when 'generate-types'
|
561
|
-
|
562
|
-
|
563
|
-
|
564
|
-
|
565
|
-
|
566
|
-
|
567
|
-
|
568
|
-
|
593
|
+
app.generate_types
|
594
|
+
SUCCESS
|
595
|
+
when 'install'
|
596
|
+
ok = outputter.spin { app.install_modules(outputter, **options.slice(:force, :resolve)) }
|
597
|
+
ok ? SUCCESS : FAILURE
|
598
|
+
when 'show'
|
599
|
+
if options[:object]
|
600
|
+
outputter.print_module_info(**app.show_module(options[:object]))
|
601
|
+
else
|
602
|
+
outputter.print_module_list(app.list_modules)
|
603
|
+
end
|
604
|
+
SUCCESS
|
569
605
|
end
|
570
|
-
code = apply_manifest(options[:code], options[:targets], options[:object], options[:noop])
|
571
|
-
else
|
572
|
-
executor = Bolt::Executor.new(config.concurrency,
|
573
|
-
analytics,
|
574
|
-
options[:noop],
|
575
|
-
config.modified_concurrency,
|
576
|
-
config.future)
|
577
|
-
targets = options[:targets]
|
578
|
-
|
579
|
-
results = nil
|
580
|
-
outputter.print_head
|
581
606
|
|
582
|
-
|
583
|
-
|
584
|
-
|
585
|
-
|
586
|
-
|
587
|
-
|
588
|
-
|
589
|
-
|
590
|
-
|
591
|
-
|
592
|
-
|
593
|
-
|
594
|
-
|
595
|
-
|
596
|
-
|
597
|
-
|
598
|
-
|
599
|
-
|
600
|
-
|
601
|
-
|
602
|
-
|
603
|
-
|
604
|
-
|
605
|
-
if src.nil?
|
606
|
-
raise Bolt::CLIError, "A source path must be specified"
|
607
|
-
end
|
608
|
-
|
609
|
-
if dest.nil?
|
610
|
-
raise Bolt::CLIError, "A destination path must be specified"
|
611
|
-
end
|
612
|
-
|
613
|
-
case options[:action]
|
614
|
-
when 'download'
|
615
|
-
dest = File.expand_path(dest, Dir.pwd)
|
616
|
-
executor.download_file(targets, src, dest, executor_opts)
|
617
|
-
when 'upload'
|
618
|
-
src_path = find_file(src, executor.future&.fetch('file_paths', false))
|
619
|
-
validate_file('source file', src_path, true)
|
620
|
-
executor.upload_file(targets, src_path, dest, executor_opts)
|
621
|
-
end
|
622
|
-
end
|
607
|
+
when 'plan'
|
608
|
+
case action
|
609
|
+
when 'convert'
|
610
|
+
app.convert_plan(options[:object])
|
611
|
+
SUCCESS
|
612
|
+
when 'new'
|
613
|
+
result = app.new_plan(options[:object], **options.slice(:puppet, :plan_script))
|
614
|
+
outputter.print_new_plan(**result)
|
615
|
+
SUCCESS
|
616
|
+
when 'run'
|
617
|
+
result = app.run_plan(options[:object], options[:targets], **options.slice(:params))
|
618
|
+
rerun.update(result)
|
619
|
+
app.shutdown
|
620
|
+
outputter.print_plan_result(result)
|
621
|
+
result.ok? ? SUCCESS : FAILURE
|
622
|
+
when 'show'
|
623
|
+
if options[:object]
|
624
|
+
outputter.print_plan_info(app.show_plan(options[:object]))
|
625
|
+
else
|
626
|
+
outputter.print_plans(**app.list_plans(**options.slice(:filter)))
|
627
|
+
end
|
628
|
+
SUCCESS
|
623
629
|
end
|
624
630
|
|
625
|
-
|
626
|
-
|
627
|
-
|
628
|
-
outputter.print_summary(results, elapsed_time)
|
629
|
-
code = results.ok ? 0 : 2
|
630
|
-
end
|
631
|
-
code
|
632
|
-
rescue Bolt::Error => e
|
633
|
-
outputter.fatal_error(e)
|
634
|
-
raise e
|
635
|
-
ensure
|
636
|
-
# restore original signal handler
|
637
|
-
Signal.trap :INT, handler if handler
|
638
|
-
analytics&.finish
|
639
|
-
end
|
640
|
-
|
641
|
-
def show_task(task_name)
|
642
|
-
outputter.print_task_info(pal.get_task(task_name))
|
643
|
-
end
|
644
|
-
|
645
|
-
# Filters a list of content by matching substring.
|
646
|
-
#
|
647
|
-
private def filter_content(content, filter)
|
648
|
-
return content unless content && filter
|
649
|
-
content.select { |name,| name.include?(filter) }
|
650
|
-
end
|
631
|
+
when 'plugin'
|
632
|
+
outputter.print_plugin_list(**app.list_plugins)
|
633
|
+
SUCCESS
|
651
634
|
|
652
|
-
|
653
|
-
|
654
|
-
|
655
|
-
|
656
|
-
|
657
|
-
|
658
|
-
|
659
|
-
|
660
|
-
|
661
|
-
def list_plans
|
662
|
-
plans = filter_content(pal.list_plans_with_cache(filter_content: true), options[:filter])
|
663
|
-
outputter.print_plans(plans, pal.user_modulepath)
|
664
|
-
end
|
635
|
+
when 'project'
|
636
|
+
case action
|
637
|
+
when 'init'
|
638
|
+
app.create_project(options[:object], outputter, **options.slice(:modules))
|
639
|
+
SUCCESS
|
640
|
+
when 'migrate'
|
641
|
+
app.migrate_project(outputter)
|
642
|
+
SUCCESS
|
643
|
+
end
|
665
644
|
|
666
|
-
|
667
|
-
|
668
|
-
|
669
|
-
|
670
|
-
|
671
|
-
|
645
|
+
when 'script'
|
646
|
+
outputter.print_head
|
647
|
+
opts = options.slice(:env_vars).merge(arguments: options[:leftovers])
|
648
|
+
results = app.run_script(options[:object], options[:targets], **opts)
|
649
|
+
rerun.update(results)
|
650
|
+
app.shutdown
|
651
|
+
outputter.print_summary(results, results.elapsed_time)
|
652
|
+
results.ok? ? SUCCESS : FAILURE
|
672
653
|
|
673
|
-
|
674
|
-
|
675
|
-
|
676
|
-
|
677
|
-
|
678
|
-
|
679
|
-
|
654
|
+
when 'secret'
|
655
|
+
case action
|
656
|
+
when 'createkeys'
|
657
|
+
result = app.create_secret_keys(**options.slice(:force, :plugin))
|
658
|
+
outputter.print_message(result)
|
659
|
+
SUCCESS
|
660
|
+
when 'decrypt'
|
661
|
+
result = app.decrypt_secret(options[:object], **options.slice(:plugin))
|
662
|
+
outputter.print_message(result)
|
663
|
+
SUCCESS
|
664
|
+
when 'encrypt'
|
665
|
+
result = app.encrypt_secret(options[:object], **options.slice(:plugin))
|
666
|
+
outputter.print_message(result)
|
667
|
+
SUCCESS
|
668
|
+
end
|
680
669
|
|
681
|
-
|
682
|
-
|
683
|
-
|
684
|
-
|
685
|
-
|
670
|
+
when 'task'
|
671
|
+
case action
|
672
|
+
when 'run'
|
673
|
+
outputter.print_head
|
674
|
+
results = app.run_task(options[:object], options[:targets], **options.slice(:params))
|
675
|
+
rerun.update(results)
|
676
|
+
app.shutdown
|
677
|
+
outputter.print_summary(results, results.elapsed_time)
|
678
|
+
results.ok? ? SUCCESS : FAILURE
|
679
|
+
when 'show'
|
680
|
+
if options[:object]
|
681
|
+
outputter.print_task_info(**app.show_task(options[:object]))
|
682
|
+
else
|
683
|
+
outputter.print_tasks(**app.list_tasks(**options.slice(:filter)))
|
684
|
+
end
|
685
|
+
SUCCESS
|
686
|
+
end
|
686
687
|
end
|
687
|
-
|
688
|
-
outputter.print_target_info(
|
689
|
-
group_targets_by_source,
|
690
|
-
inventory.source,
|
691
|
-
config.default_inventoryfile,
|
692
|
-
target_flag
|
693
|
-
)
|
694
688
|
end
|
695
689
|
|
696
|
-
#
|
697
|
-
#
|
690
|
+
# Process the target list by turning a PuppetDB query or rerun mode into a
|
691
|
+
# list of target names.
|
698
692
|
#
|
699
|
-
|
700
|
-
|
701
|
-
|
702
|
-
|
703
|
-
|
704
|
-
|
705
|
-
|
706
|
-
|
707
|
-
|
693
|
+
# @param pdb_client [Bolt::PuppetDB::Client] The PuppetDB client.
|
694
|
+
# @param rerun [Bolt::Rerun] The Rerun instance.
|
695
|
+
# @param options [Hash] The CLI options.
|
696
|
+
# @return [Hash] The target list.
|
697
|
+
#
|
698
|
+
private def process_target_list(pdb_client, rerun, options)
|
699
|
+
if options[:query]
|
700
|
+
pdb_client.query_certnames(options[:query])
|
701
|
+
elsif options[:rerun]
|
702
|
+
rerun.get_targets(options[:rerun])
|
703
|
+
elsif options[:targets]
|
704
|
+
options[:targets]
|
708
705
|
end
|
709
|
-
|
710
|
-
{ inventory: inventory_targets, adhoc: adhoc_targets }
|
711
|
-
end
|
712
|
-
|
713
|
-
def list_groups
|
714
|
-
outputter.print_groups(inventory.group_names.sort, inventory.source, config.default_inventoryfile)
|
715
706
|
end
|
716
707
|
|
717
|
-
#
|
718
|
-
# provided variable values for interpolations
|
708
|
+
# List content that ships with Bolt.
|
719
709
|
#
|
720
|
-
|
721
|
-
result = pal.plan_hierarchy_lookup(key, plan_vars: plan_vars)
|
722
|
-
outputter.print_plan_lookup(result)
|
723
|
-
0
|
724
|
-
end
|
725
|
-
|
726
|
-
# Looks up a value with Hiera, using targets as the contexts to perform the
|
727
|
-
# look ups in. This should return the same value as a lookup in an apply block.
|
710
|
+
# @param options [Hash] The CLI options.
|
728
711
|
#
|
729
|
-
def
|
730
|
-
|
731
|
-
|
732
|
-
|
733
|
-
|
734
|
-
|
735
|
-
|
736
|
-
|
737
|
-
|
738
|
-
executor.subscribe(outputter) if config.format == 'human'
|
739
|
-
executor.subscribe(log_outputter)
|
740
|
-
executor.publish_event(type: :plan_start, plan: nil)
|
741
|
-
|
742
|
-
results = outputter.spin do
|
743
|
-
pal.lookup(
|
744
|
-
key,
|
745
|
-
targets,
|
746
|
-
inventory,
|
747
|
-
executor,
|
748
|
-
plan_vars: plan_vars
|
749
|
-
)
|
750
|
-
end
|
751
|
-
|
752
|
-
executor.shutdown
|
753
|
-
outputter.print_result_set(results)
|
754
|
-
|
755
|
-
results.ok ? 0 : 1
|
756
|
-
end
|
757
|
-
|
758
|
-
def run_plan(plan_name, plan_arguments, nodes, options)
|
759
|
-
unless nodes.empty?
|
760
|
-
if plan_arguments['nodes'] || plan_arguments['targets']
|
761
|
-
key = plan_arguments.include?('nodes') ? 'nodes' : 'targets'
|
762
|
-
raise Bolt::CLIError,
|
763
|
-
"A plan's '#{key}' parameter can be specified using the --#{key} option, but in that " \
|
764
|
-
"case it must not be specified as a separate #{key}=<value> parameter nor included " \
|
765
|
-
"in the JSON data passed in the --params option"
|
712
|
+
private def bundled_content(options)
|
713
|
+
# We only need to enumerate bundled content when running a task or plan
|
714
|
+
content = { 'Plan' => [],
|
715
|
+
'Task' => [],
|
716
|
+
'Plugin' => Bolt::Plugin::BUILTIN_PLUGINS }
|
717
|
+
if %w[plan task].include?(options[:subcommand]) && options[:action] == 'run'
|
718
|
+
default_content = Bolt::PAL.new(Bolt::Config::Modulepath.new([]), nil, nil)
|
719
|
+
content['Plan'] = default_content.list_plans.each_with_object([]) do |iter, col|
|
720
|
+
col << iter&.first
|
766
721
|
end
|
767
|
-
|
768
|
-
|
769
|
-
target_param = plan_params.dig('targets', 'type') =~ /TargetSpec/
|
770
|
-
node_param = plan_params.include?('nodes')
|
771
|
-
|
772
|
-
if node_param && target_param
|
773
|
-
msg = "Plan parameters include both 'nodes' and 'targets' with type 'TargetSpec', " \
|
774
|
-
"neither will populated with the value for --nodes or --targets."
|
775
|
-
Bolt::Logger.warn("nodes_targets_parameters", msg)
|
776
|
-
elsif node_param
|
777
|
-
plan_arguments['nodes'] = nodes.join(',')
|
778
|
-
elsif target_param
|
779
|
-
plan_arguments['targets'] = nodes.join(',')
|
722
|
+
content['Task'] = default_content.list_tasks.each_with_object([]) do |iter, col|
|
723
|
+
col << iter&.first
|
780
724
|
end
|
781
725
|
end
|
782
726
|
|
783
|
-
|
784
|
-
params: plan_arguments }
|
785
|
-
|
786
|
-
executor = Bolt::Executor.new(config.concurrency,
|
787
|
-
analytics,
|
788
|
-
options[:noop],
|
789
|
-
config.modified_concurrency,
|
790
|
-
config.future)
|
791
|
-
if %w[human rainbow].include?(options.fetch(:format, 'human'))
|
792
|
-
executor.subscribe(outputter)
|
793
|
-
else
|
794
|
-
# Only subscribe to out module events for JSON outputter
|
795
|
-
executor.subscribe(outputter, %i[message verbose])
|
796
|
-
end
|
797
|
-
|
798
|
-
executor.subscribe(log_outputter)
|
799
|
-
executor.start_plan(plan_context)
|
800
|
-
result = pal.run_plan(plan_name, plan_arguments, executor, inventory, puppetdb_client)
|
801
|
-
|
802
|
-
# If a non-bolt exception bubbles up the plan won't get finished
|
803
|
-
executor.finish_plan(result)
|
804
|
-
executor.shutdown
|
805
|
-
rerun.update(result)
|
806
|
-
|
807
|
-
outputter.print_plan_result(result)
|
808
|
-
result.ok? ? 0 : 1
|
727
|
+
content
|
809
728
|
end
|
810
729
|
|
811
|
-
|
812
|
-
|
813
|
-
|
814
|
-
|
815
|
-
|
816
|
-
|
817
|
-
|
818
|
-
|
819
|
-
"Definitions must be declared for their resources to be applied. You can read more "\
|
820
|
-
"about defining and declaring classes and types in the Puppet documentation at "\
|
821
|
-
"https://puppet.com/docs/puppet/latest/lang_classes.html and "\
|
822
|
-
"https://puppet.com/docs/puppet/latest/lang_defined_types.html"
|
823
|
-
Bolt::Logger.warn("empty_manifest", message)
|
824
|
-
end
|
730
|
+
# Check and warn if Bolt is installed as a gem.
|
731
|
+
#
|
732
|
+
private def check_gem_install
|
733
|
+
if ENV['BOLT_GEM'].nil? && incomplete_install?
|
734
|
+
msg = <<~MSG.chomp
|
735
|
+
Bolt might be installed as a gem. To use Bolt reliably and with all of its
|
736
|
+
dependencies, uninstall the 'bolt' gem and install Bolt as a package:
|
737
|
+
https://puppet.com/docs/bolt/latest/bolt_installing.html
|
825
738
|
|
826
|
-
|
827
|
-
|
828
|
-
|
829
|
-
config.modified_concurrency,
|
830
|
-
config.future)
|
831
|
-
executor.subscribe(outputter) if options.fetch(:format, 'human') == 'human'
|
832
|
-
executor.subscribe(log_outputter)
|
833
|
-
# apply logging looks like plan logging, so tell the outputter we're in a
|
834
|
-
# plan even though we're not
|
835
|
-
executor.publish_event(type: :plan_start, plan: nil)
|
836
|
-
|
837
|
-
results = nil
|
838
|
-
elapsed_time = Benchmark.realtime do
|
839
|
-
pal.in_plan_compiler(executor, inventory, puppetdb_client) do |compiler|
|
840
|
-
compiler.call_function('apply_prep', targets)
|
841
|
-
end
|
739
|
+
If you meant to install Bolt as a gem and want to disable this warning,
|
740
|
+
set the BOLT_GEM environment variable.
|
741
|
+
MSG
|
842
742
|
|
843
|
-
|
844
|
-
Puppet.lookup(:apply_executor).apply_ast(ast, targets, catch_errors: true, noop: noop)
|
845
|
-
end
|
743
|
+
Bolt::Logger.warn("gem_install", msg)
|
846
744
|
end
|
847
|
-
|
848
|
-
executor.shutdown
|
849
|
-
outputter.print_apply_result(results, elapsed_time)
|
850
|
-
rerun.update(results)
|
851
|
-
|
852
|
-
results.ok ? 0 : 1
|
853
|
-
end
|
854
|
-
|
855
|
-
def list_modules
|
856
|
-
outputter.print_module_list(pal.list_modules)
|
857
|
-
end
|
858
|
-
|
859
|
-
def show_module(name)
|
860
|
-
outputter.print_module_info(**pal.show_module(name))
|
861
|
-
end
|
862
|
-
|
863
|
-
def list_plugins
|
864
|
-
outputter.print_plugin_list(plugins.list_plugins, pal.user_modulepath)
|
865
|
-
end
|
866
|
-
|
867
|
-
def generate_types
|
868
|
-
# generate_types will surface a nice error with helpful message if it fails
|
869
|
-
pal.generate_types(cache: true)
|
870
|
-
0
|
871
745
|
end
|
872
746
|
|
873
|
-
#
|
747
|
+
# Print a fatal error. Print using the outputter if it's configured.
|
748
|
+
# Otherwise, mock the output by printing directly to stdout.
|
874
749
|
#
|
875
|
-
|
876
|
-
|
877
|
-
|
878
|
-
if
|
879
|
-
outputter.
|
880
|
-
|
881
|
-
|
882
|
-
|
883
|
-
|
884
|
-
end
|
885
|
-
|
886
|
-
installer = Bolt::ModuleInstaller.new(outputter, pal)
|
887
|
-
|
888
|
-
ok = outputter.spin do
|
889
|
-
installer.install(project.modules,
|
890
|
-
project.puppetfile,
|
891
|
-
project.managed_moduledir,
|
892
|
-
config,
|
893
|
-
force: force,
|
894
|
-
resolve: resolve)
|
750
|
+
# @param error [StandardError] The error to print.
|
751
|
+
#
|
752
|
+
private def fatal_error(error)
|
753
|
+
if @outputter
|
754
|
+
@outputter.fatal_error(error)
|
755
|
+
elsif $stdout.isatty
|
756
|
+
$stdout.puts("\033[31m#{error.message}\033[0m")
|
757
|
+
else
|
758
|
+
$stdout.puts(error.message)
|
895
759
|
end
|
896
|
-
|
897
|
-
ok ? 0 : 1
|
898
760
|
end
|
899
761
|
|
900
|
-
#
|
762
|
+
# Query whether Bolt is installed as a gem or package by checking if all
|
763
|
+
# built-in modules are installed.
|
901
764
|
#
|
902
|
-
def
|
903
|
-
|
904
|
-
|
905
|
-
installer = Bolt::ModuleInstaller.new(outputter, pal)
|
906
|
-
|
907
|
-
ok = outputter.spin do
|
908
|
-
installer.add(name,
|
909
|
-
project.modules,
|
910
|
-
project.puppetfile,
|
911
|
-
project.managed_moduledir,
|
912
|
-
project.project_file,
|
913
|
-
config)
|
914
|
-
end
|
915
|
-
|
916
|
-
ok ? 0 : 1
|
765
|
+
private def incomplete_install?
|
766
|
+
builtin_module_list = %w[aggregate canary puppetdb_fact secure_env_vars puppet_connect]
|
767
|
+
(Dir.children(Bolt::Config::Modulepath::MODULES_PATH) - builtin_module_list).empty?
|
917
768
|
end
|
918
769
|
|
919
|
-
#
|
770
|
+
# Parse parameters for tasks and plans.
|
920
771
|
#
|
921
|
-
|
922
|
-
|
923
|
-
|
924
|
-
|
925
|
-
|
926
|
-
|
927
|
-
|
928
|
-
raise Bolt::Error.new(msg, 'bolt/missing-project-config-error')
|
772
|
+
# @param options [Hash] Options from the calling method.
|
773
|
+
#
|
774
|
+
private def parse_params(command, object, pal, params: nil, params_parsed: nil)
|
775
|
+
if params
|
776
|
+
params_parsed ? params : pal.parse_params(command, object, params)
|
777
|
+
else
|
778
|
+
{}
|
929
779
|
end
|
930
780
|
end
|
931
781
|
|
932
|
-
#
|
782
|
+
# Parse variables for lookups.
|
933
783
|
#
|
934
|
-
|
935
|
-
|
936
|
-
|
937
|
-
|
938
|
-
|
939
|
-
end
|
940
|
-
|
941
|
-
ok ? 0 : 1
|
784
|
+
# @param vars [Array, NilClass] Unparsed variables.
|
785
|
+
#
|
786
|
+
private def parse_vars(vars)
|
787
|
+
return unless vars
|
788
|
+
Hash[vars.map { |a| a.split('=', 2) }]
|
942
789
|
end
|
943
790
|
|
944
|
-
|
945
|
-
|
946
|
-
|
947
|
-
|
948
|
-
|
949
|
-
|
950
|
-
|
951
|
-
|
952
|
-
|
791
|
+
# TODO: See if this can be moved to Bolt::Analytics.
|
792
|
+
#
|
793
|
+
# Submit a screen view to the analytics client.
|
794
|
+
#
|
795
|
+
# @param analytics [Bolt::Analytics] The analytics client.
|
796
|
+
# @param config [Bolt::Config] The config.
|
797
|
+
# @param inventory [Bolt::Inventory] The inventory.
|
798
|
+
# @param options [Hash] The CLI options.
|
799
|
+
#
|
800
|
+
private def submit_screen_view(analytics, config, inventory, options)
|
801
|
+
screen = "#{options[:subcommand]}_#{options[:action]}"
|
953
802
|
|
954
|
-
|
955
|
-
|
956
|
-
@guides ||= begin
|
957
|
-
root_path = File.expand_path(File.join(__dir__, '..', '..', 'guides'))
|
958
|
-
files = Dir.children(root_path).sort
|
959
|
-
|
960
|
-
files.each_with_object({}) do |file, guides|
|
961
|
-
next if file !~ /\.(yaml|yml)\z/
|
962
|
-
# The ".*" here removes any suffix
|
963
|
-
topic = File.basename(file, ".*")
|
964
|
-
guides[topic] = File.join(root_path, file)
|
965
|
-
end
|
966
|
-
rescue SystemCallError => e
|
967
|
-
raise Bolt::FileError.new("#{e.message}: unable to load guides directory", root_path)
|
803
|
+
if options[:action] == 'show' && options[:object]
|
804
|
+
screen += '_object'
|
968
805
|
end
|
969
|
-
end
|
970
806
|
|
971
|
-
|
972
|
-
|
973
|
-
|
974
|
-
|
975
|
-
|
976
|
-
|
977
|
-
|
978
|
-
|
979
|
-
|
980
|
-
analytics.event('Guide', 'known_topic', label: topic)
|
981
|
-
|
982
|
-
begin
|
983
|
-
guide = Bolt::Util.read_yaml_hash(guides[topic], 'guide')
|
984
|
-
rescue SystemCallError => e
|
985
|
-
raise Bolt::FileError("#{e.message}: unable to load guide page", filepath)
|
986
|
-
end
|
807
|
+
pp_count, yaml_count = if File.exist?(config.project.plans_path)
|
808
|
+
%w[pp yaml].map do |extension|
|
809
|
+
Find.find(config.project.plans_path.to_s)
|
810
|
+
.grep(/.*\.#{extension}/)
|
811
|
+
.length
|
812
|
+
end
|
813
|
+
else
|
814
|
+
[0, 0]
|
815
|
+
end
|
987
816
|
|
988
|
-
|
989
|
-
|
990
|
-
|
991
|
-
|
992
|
-
|
993
|
-
|
994
|
-
outputter.print_guide(**Bolt::Util.symbolize_top_level_keys(guide))
|
995
|
-
else
|
996
|
-
analytics.event('Guide', 'unknown_topic', label: topic)
|
997
|
-
outputter.print_message("Did not find guide for topic '#{topic}'.\n\n")
|
998
|
-
list_topics
|
999
|
-
end
|
1000
|
-
0
|
1001
|
-
end
|
817
|
+
screen_view_fields = {
|
818
|
+
output_format: config.format,
|
819
|
+
boltdir_type: config.project.type,
|
820
|
+
puppet_plan_count: pp_count,
|
821
|
+
yaml_plan_count: yaml_count
|
822
|
+
}
|
1002
823
|
|
1003
|
-
|
1004
|
-
|
1005
|
-
|
824
|
+
if options.key?(:targets)
|
825
|
+
screen_view_fields.merge!(
|
826
|
+
target_nodes: options[:targets].count,
|
827
|
+
inventory_nodes: inventory.node_names.count,
|
828
|
+
inventory_groups: inventory.group_names.count,
|
829
|
+
inventory_version: inventory.version
|
830
|
+
)
|
1006
831
|
end
|
1007
832
|
|
1008
|
-
|
833
|
+
analytics.screen_view(screen, **screen_view_fields)
|
1009
834
|
end
|
1010
835
|
|
1011
|
-
#
|
1012
|
-
#
|
1013
|
-
# the path is a Puppet file path and looks for the file in a module's files
|
1014
|
-
# directory.
|
836
|
+
# Issue a deprecation warning if the user is running an unsupported version
|
837
|
+
# of PowerShell on the controller.
|
1015
838
|
#
|
1016
|
-
def
|
1017
|
-
|
1018
|
-
|
1019
|
-
|
1020
|
-
|
1021
|
-
|
1022
|
-
if modules[mod]
|
1023
|
-
@logger.debug("Did not find file at #{File.expand_path(path)}, checking in module '#{mod}'")
|
1024
|
-
found = Bolt::Util.find_file_in_module(modules[mod].path, file || "", future_file_paths)
|
1025
|
-
path = found.nil? ? File.join(modules[mod].path, 'files', file) : found
|
1026
|
-
end
|
1027
|
-
path
|
1028
|
-
end
|
839
|
+
private def validate_ps_version
|
840
|
+
if Bolt::Util.powershell?
|
841
|
+
command = "powershell.exe -NoProfile -NonInteractive -NoLogo -ExecutionPolicy "\
|
842
|
+
"Bypass -Command $PSVersionTable.PSVersion.Major"
|
843
|
+
stdout, _stderr, _status = Open3.capture3(command)
|
1029
844
|
|
1030
|
-
|
1031
|
-
@rerun ||= Bolt::Rerun.new(config.rerunfile, config.save_rerun)
|
1032
|
-
end
|
845
|
+
return unless !stdout.empty? && stdout.to_i < 3
|
1033
846
|
|
1034
|
-
|
1035
|
-
|
1036
|
-
|
1037
|
-
options[:verbose],
|
1038
|
-
config.trace,
|
1039
|
-
config.spinner)
|
847
|
+
msg = "Detected PowerShell 2 on controller. PowerShell 2 is unsupported."
|
848
|
+
Bolt::Logger.deprecation_warning("powershell_2_controller", msg)
|
849
|
+
end
|
1040
850
|
end
|
1041
851
|
|
1042
|
-
|
1043
|
-
|
1044
|
-
|
852
|
+
# Warn the user that transport configuration options set from the command
|
853
|
+
# line may be overridden by transport configuration set in the inventory.
|
854
|
+
#
|
855
|
+
# @param opts [Hash] The CLI options.
|
856
|
+
#
|
857
|
+
private def warn_inventory_overrides_cli(config, opts)
|
858
|
+
inventory_source = if ENV[Bolt::Inventory::ENVIRONMENT_VAR]
|
859
|
+
Bolt::Inventory::ENVIRONMENT_VAR
|
860
|
+
elsif config.inventoryfile
|
861
|
+
config.inventoryfile
|
862
|
+
elsif File.exist?(config.default_inventoryfile)
|
863
|
+
config.default_inventoryfile
|
864
|
+
end
|
1045
865
|
|
1046
|
-
|
1047
|
-
|
1048
|
-
client = Bolt::Analytics.build_client(config.analytics)
|
1049
|
-
client.bundled_content = bundled_content
|
1050
|
-
client
|
866
|
+
inventory_cli_opts = %i[authentication escalation transports].each_with_object([]) do |key, acc|
|
867
|
+
acc.concat(Bolt::BoltOptionParser::OPTIONS[key])
|
1051
868
|
end
|
1052
|
-
end
|
1053
869
|
|
1054
|
-
|
1055
|
-
# If the bundled content directory is empty, Bolt is likely installed as a gem.
|
1056
|
-
if ENV['BOLT_GEM'].nil? && incomplete_install?
|
1057
|
-
msg = <<~MSG.chomp
|
1058
|
-
Bolt might be installed as a gem. To use Bolt reliably and with all of its
|
1059
|
-
dependencies, uninstall the 'bolt' gem and install Bolt as a package:
|
1060
|
-
https://puppet.com/docs/bolt/latest/bolt_installing.html
|
1061
|
-
|
1062
|
-
If you meant to install Bolt as a gem and want to disable this warning,
|
1063
|
-
set the BOLT_GEM environment variable.
|
1064
|
-
MSG
|
870
|
+
inventory_cli_opts.concat(%w[no-host-key-check no-ssl no-ssl-verify no-tty])
|
1065
871
|
|
1066
|
-
|
1067
|
-
end
|
872
|
+
conflicting_options = Set.new(opts.keys.map(&:to_s)).intersection(inventory_cli_opts)
|
1068
873
|
|
1069
|
-
|
1070
|
-
|
1071
|
-
|
1072
|
-
|
1073
|
-
|
1074
|
-
default_content = Bolt::PAL.new(Bolt::Config::Modulepath.new([]), nil, nil)
|
1075
|
-
content['Plan'] = default_content.list_plans.each_with_object([]) do |iter, col|
|
1076
|
-
col << iter&.first
|
1077
|
-
end
|
1078
|
-
content['Task'] = default_content.list_tasks.each_with_object([]) do |iter, col|
|
1079
|
-
col << iter&.first
|
1080
|
-
end
|
874
|
+
if inventory_source && conflicting_options.any?
|
875
|
+
Bolt::Logger.warn(
|
876
|
+
"cli_overrides",
|
877
|
+
"CLI arguments #{conflicting_options.to_a} might be overridden by Inventory: #{inventory_source}"
|
878
|
+
)
|
1081
879
|
end
|
1082
|
-
|
1083
|
-
content
|
1084
880
|
end
|
1085
881
|
|
1086
|
-
#
|
1087
|
-
#
|
1088
|
-
def
|
1089
|
-
|
1090
|
-
|
882
|
+
# Handle and print errors.
|
883
|
+
#
|
884
|
+
private def with_error_handling
|
885
|
+
yield
|
886
|
+
rescue Bolt::Error => e
|
887
|
+
fatal_error(e)
|
888
|
+
raise e
|
1091
889
|
end
|
1092
890
|
|
1093
|
-
#
|
1094
|
-
#
|
1095
|
-
def
|
1096
|
-
|
1097
|
-
|
1098
|
-
|
1099
|
-
|
891
|
+
# Handle signals.
|
892
|
+
#
|
893
|
+
private def with_signal_handling
|
894
|
+
handler = Signal.trap :INT do |signo|
|
895
|
+
Bolt::Logger.logger(self).info(
|
896
|
+
"Exiting after receiving SIG#{Signal.signame(signo)} signal. "\
|
897
|
+
"There might be processes left executing on some targets."
|
898
|
+
)
|
899
|
+
exit!
|
1100
900
|
end
|
901
|
+
|
902
|
+
yield
|
903
|
+
ensure
|
904
|
+
Signal.trap :INT, handler if handler
|
1101
905
|
end
|
1102
906
|
end
|
1103
907
|
end
|