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