bolt 3.13.0 → 3.16.1
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/bolt-modules/boltlib/lib/puppet/functions/background.rb +2 -1
- data/bolt-modules/boltlib/lib/puppet/functions/parallelize.rb +5 -1
- data/bolt-modules/boltlib/lib/puppet/functions/run_plan.rb +13 -0
- data/bolt-modules/boltlib/lib/puppet/functions/wait.rb +47 -7
- data/bolt-modules/out/lib/puppet/functions/out/message.rb +4 -2
- data/bolt-modules/out/lib/puppet/functions/out/verbose.rb +4 -2
- data/guides/{debugging.txt → debugging.yaml} +5 -6
- data/guides/{inventory.txt → inventory.yaml} +6 -7
- data/guides/{links.txt → links.yaml} +3 -4
- data/guides/{logging.txt → logging.yaml} +5 -6
- data/guides/{module.txt → module.yaml} +5 -6
- data/guides/{modulepath.txt → modulepath.yaml} +5 -6
- data/guides/{project.txt → project.yaml} +6 -7
- data/guides/{targets.txt → targets.yaml} +5 -6
- data/guides/{transports.txt → transports.yaml} +6 -7
- data/lib/bolt/analytics.rb +3 -20
- data/lib/bolt/application.rb +620 -0
- data/lib/bolt/bolt_option_parser.rb +17 -5
- data/lib/bolt/cli.rb +592 -772
- data/lib/bolt/config/transport/options.rb +12 -0
- data/lib/bolt/config/transport/ssh.rb +7 -0
- data/lib/bolt/executor.rb +12 -4
- data/lib/bolt/fiber_executor.rb +63 -14
- data/lib/bolt/module_installer/puppetfile.rb +24 -10
- data/lib/bolt/outputter/human.rb +199 -43
- data/lib/bolt/outputter/json.rb +66 -43
- data/lib/bolt/outputter/logger.rb +1 -1
- data/lib/bolt/pal.rb +67 -14
- data/lib/bolt/pal/yaml_plan/step.rb +2 -0
- data/lib/bolt/pal/yaml_plan/step/message.rb +0 -8
- data/lib/bolt/pal/yaml_plan/step/verbose.rb +31 -0
- data/lib/bolt/pal/yaml_plan/transpiler.rb +1 -1
- data/lib/bolt/plan_creator.rb +2 -20
- data/lib/bolt/plan_future.rb +23 -3
- data/lib/bolt/plan_result.rb +1 -1
- data/lib/bolt/plugin/task.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/transport/ssh/exec_connection.rb +3 -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/schemas/partials/target-ssh.json +4 -0
- data/lib/bolt_server/schemas/partials/target-winrm.json +4 -0
- data/lib/bolt_server/transport_app.rb +180 -60
- data/lib/bolt_spec/plans/mock_executor.rb +16 -6
- metadata +23 -15
- data/guides/guide.txt +0 -17
- data/lib/bolt/secret.rb +0 -37
@@ -507,11 +507,14 @@ module Bolt
|
|
507
507
|
show
|
508
508
|
|
509
509
|
#{colorize(:cyan, 'Usage')}
|
510
|
-
bolt module show [options]
|
510
|
+
bolt module show [module name] [options]
|
511
511
|
|
512
512
|
#{colorize(:cyan, 'Description')}
|
513
513
|
List modules available to the Bolt project.
|
514
514
|
|
515
|
+
Providing the name of a module will display detailed documentation for
|
516
|
+
the module.
|
517
|
+
|
515
518
|
#{colorize(:cyan, 'Documentation')}
|
516
519
|
To learn more about Bolt modules, run 'bolt guide module'.
|
517
520
|
HELP
|
@@ -894,7 +897,7 @@ module Bolt
|
|
894
897
|
end
|
895
898
|
define('--params PARAMETERS',
|
896
899
|
"Parameters to a task or plan as json, a json file '@<file>', or on stdin '-'.") do |params|
|
897
|
-
@options[:
|
900
|
+
@options[:params] = parse_params(params)
|
898
901
|
end
|
899
902
|
define('-e', '--execute CODE',
|
900
903
|
"Puppet manifest code to apply to the targets.") do |code|
|
@@ -1083,8 +1086,7 @@ module Bolt
|
|
1083
1086
|
@options[:help] = true
|
1084
1087
|
end
|
1085
1088
|
define('--version', 'Display the version.') do |_|
|
1086
|
-
|
1087
|
-
raise Bolt::CLIExit
|
1089
|
+
@options[:version] = true
|
1088
1090
|
end
|
1089
1091
|
define('--log-level LEVEL',
|
1090
1092
|
"Set the log level for the console. Available options are",
|
@@ -1092,7 +1094,7 @@ module Bolt
|
|
1092
1094
|
@options[:log] = { 'console' => { 'level' => level } }
|
1093
1095
|
end
|
1094
1096
|
define('--clear-cache',
|
1095
|
-
"Clear plugin
|
1097
|
+
"Clear plugin, plan, and task caches before executing.") do |_|
|
1096
1098
|
@options[:clear_cache] = true
|
1097
1099
|
end
|
1098
1100
|
define('--plugin PLUGIN', 'Select the plugin to use.') do |plug|
|
@@ -1127,5 +1129,15 @@ module Bolt
|
|
1127
1129
|
rescue JSON::ParserError => e
|
1128
1130
|
raise Bolt::CLIError, "Unable to parse --params value as JSON: #{e}"
|
1129
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
|
1130
1142
|
end
|
1131
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,149 +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] && 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)
|
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)
|
288
224
|
unless COMMANDS.include?(options[:subcommand])
|
289
225
|
command = Bolt::Util.powershell? ? 'Get-Command -Module PuppetBolt' : 'bolt help'
|
290
226
|
raise Bolt::CLIError,
|
@@ -349,12 +285,34 @@ module Bolt
|
|
349
285
|
raise Bolt::CLIError, "Must specify a module name."
|
350
286
|
end
|
351
287
|
|
288
|
+
if options[:action] == 'convert' && !options[:object]
|
289
|
+
raise Bolt::CLIError, "Must specify a plan."
|
290
|
+
end
|
291
|
+
|
352
292
|
if options[:subcommand] == 'module' && options[:action] == 'install' && options[:object]
|
353
293
|
command = Bolt::Util.powershell? ? 'Add-BoltModule -Module' : 'bolt module add'
|
354
294
|
raise Bolt::CLIError, "Invalid argument '#{options[:object]}'. To add a new module to "\
|
355
295
|
"the project, run '#{command} #{options[:object]}'."
|
356
296
|
end
|
357
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
|
+
|
358
316
|
if !%w[file script lookup].include?(options[:subcommand]) &&
|
359
317
|
!options[:leftovers].empty?
|
360
318
|
raise Bolt::CLIError,
|
@@ -379,710 +337,572 @@ module Bolt
|
|
379
337
|
"Option '--env-var' can only be specified when running a command or script"
|
380
338
|
end
|
381
339
|
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
|
-
|
394
|
-
def puppetdb_client
|
395
|
-
plugins.puppetdb_client
|
396
|
-
end
|
397
340
|
|
398
|
-
|
399
|
-
@plugins ||= Bolt::Plugin.setup(config, pal, analytics)
|
341
|
+
validate_targeting_options(options)
|
400
342
|
end
|
401
343
|
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
|
407
|
-
|
408
|
-
|
409
|
-
|
410
|
-
config.inventoryfile
|
411
|
-
elsif File.exist?(config.default_inventoryfile)
|
412
|
-
config.default_inventoryfile
|
413
|
-
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'"
|
414
352
|
|
415
|
-
|
416
|
-
|
353
|
+
if target_opts.length > 1
|
354
|
+
raise Bolt::CLIError, "Only one targeting option can be specified: #{target_string}"
|
417
355
|
end
|
418
356
|
|
419
|
-
|
357
|
+
return if %w[guide module plan project secret].include?(options[:subcommand]) ||
|
358
|
+
%w[convert new show].include?(options[:action]) ||
|
359
|
+
options[:plan_hierarchy]
|
420
360
|
|
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
|
-
)
|
361
|
+
if target_opts.empty?
|
362
|
+
raise Bolt::CLIError, "Command requires a targeting option: #{target_string}"
|
428
363
|
end
|
429
364
|
end
|
430
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
|
+
#
|
431
372
|
def execute(options)
|
432
|
-
|
433
|
-
|
434
|
-
|
435
|
-
|
436
|
-
|
437
|
-
|
438
|
-
|
439
|
-
|
440
|
-
|
441
|
-
|
442
|
-
|
443
|
-
|
444
|
-
|
445
|
-
|
446
|
-
|
447
|
-
|
448
|
-
|
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
|
449
395
|
|
450
|
-
|
451
|
-
# submit a different screen for `bolt task show` and `bolt task show foo`
|
452
|
-
if options[:action] == 'show' && options[:object]
|
453
|
-
screen += '_object'
|
454
|
-
end
|
396
|
+
config = Bolt::Config.from_project(project, options)
|
455
397
|
|
456
|
-
|
457
|
-
|
458
|
-
|
459
|
-
|
460
|
-
|
398
|
+
@outputter = Bolt::Outputter.for_format(
|
399
|
+
config.format,
|
400
|
+
config.color,
|
401
|
+
options[:verbose],
|
402
|
+
config.trace,
|
403
|
+
config.spinner
|
404
|
+
)
|
461
405
|
|
462
|
-
|
463
|
-
# list. This avoids loading inventory for commands that don't need it.
|
464
|
-
if options.key?(:targets)
|
465
|
-
screen_view_fields.merge!(target_nodes: options[:targets].count,
|
466
|
-
inventory_nodes: inventory.node_names.count,
|
467
|
-
inventory_groups: inventory.group_names.count,
|
468
|
-
inventory_version: inventory.version)
|
469
|
-
end
|
406
|
+
@rerun = Bolt::Rerun.new(config.rerunfile, config.save_rerun)
|
470
407
|
|
471
|
-
|
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
|
413
|
+
end
|
472
414
|
|
473
|
-
|
474
|
-
|
475
|
-
|
476
|
-
|
477
|
-
|
478
|
-
|
479
|
-
|
480
|
-
|
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)
|
481
460
|
end
|
482
|
-
|
483
|
-
|
484
|
-
|
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
|
485
473
|
else
|
486
|
-
|
474
|
+
executor.subscribe(outputter)
|
487
475
|
end
|
488
|
-
|
489
|
-
|
490
|
-
|
491
|
-
|
492
|
-
|
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
|
+
)
|
493
489
|
end
|
494
|
-
|
495
|
-
|
496
|
-
|
497
|
-
|
498
|
-
|
499
|
-
|
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
|
500
503
|
end
|
501
|
-
return 0
|
502
|
-
when 'convert'
|
503
|
-
pal.convert_plan(options[:object])
|
504
|
-
return 0
|
505
504
|
end
|
505
|
+
end
|
506
|
+
|
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
|
506
524
|
|
507
|
-
|
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
|
508
550
|
|
509
|
-
|
510
|
-
|
511
|
-
|
551
|
+
when 'group'
|
552
|
+
outputter.print_groups(**app.list_groups)
|
553
|
+
SUCCESS
|
512
554
|
|
513
|
-
case options[:subcommand]
|
514
555
|
when 'guide'
|
515
|
-
|
516
|
-
|
517
|
-
|
518
|
-
|
519
|
-
end
|
520
|
-
when 'project'
|
521
|
-
case options[:action]
|
522
|
-
when 'init'
|
523
|
-
code = Bolt::ProjectManager.new(config, outputter, pal)
|
524
|
-
.create(Dir.pwd, options[:object], options[:modules])
|
525
|
-
when 'migrate'
|
526
|
-
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)
|
527
560
|
end
|
528
|
-
|
529
|
-
|
530
|
-
|
531
|
-
|
532
|
-
|
533
|
-
|
534
|
-
|
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)
|
535
570
|
end
|
536
|
-
|
537
|
-
|
538
|
-
|
539
|
-
|
540
|
-
|
541
|
-
|
542
|
-
|
543
|
-
|
544
|
-
|
545
|
-
|
546
|
-
|
547
|
-
|
548
|
-
|
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
|
549
586
|
end
|
587
|
+
|
550
588
|
when 'module'
|
551
|
-
case
|
589
|
+
case action
|
552
590
|
when 'add'
|
553
|
-
|
554
|
-
|
555
|
-
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
|
556
593
|
when 'generate-types'
|
557
|
-
|
558
|
-
|
559
|
-
|
560
|
-
|
561
|
-
|
562
|
-
|
563
|
-
|
564
|
-
|
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
|
565
606
|
end
|
566
|
-
code = apply_manifest(options[:code], options[:targets], options[:object], options[:noop])
|
567
|
-
else
|
568
|
-
executor = Bolt::Executor.new(config.concurrency,
|
569
|
-
analytics,
|
570
|
-
options[:noop],
|
571
|
-
config.modified_concurrency,
|
572
|
-
config.future)
|
573
|
-
targets = options[:targets]
|
574
|
-
|
575
|
-
results = nil
|
576
|
-
outputter.print_head
|
577
607
|
|
578
|
-
|
579
|
-
|
580
|
-
|
581
|
-
|
582
|
-
|
583
|
-
|
584
|
-
|
585
|
-
|
586
|
-
|
587
|
-
|
588
|
-
|
589
|
-
|
590
|
-
|
591
|
-
|
592
|
-
|
593
|
-
|
594
|
-
|
595
|
-
|
596
|
-
|
597
|
-
|
598
|
-
|
599
|
-
|
600
|
-
|
601
|
-
if src.nil?
|
602
|
-
raise Bolt::CLIError, "A source path must be specified"
|
603
|
-
end
|
604
|
-
|
605
|
-
if dest.nil?
|
606
|
-
raise Bolt::CLIError, "A destination path must be specified"
|
607
|
-
end
|
608
|
-
|
609
|
-
case options[:action]
|
610
|
-
when 'download'
|
611
|
-
dest = File.expand_path(dest, Dir.pwd)
|
612
|
-
executor.download_file(targets, src, dest, executor_opts)
|
613
|
-
when 'upload'
|
614
|
-
src_path = find_file(src, executor.future&.fetch('file_paths', false))
|
615
|
-
validate_file('source file', src_path, true)
|
616
|
-
executor.upload_file(targets, src_path, dest, executor_opts)
|
617
|
-
end
|
618
|
-
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
|
619
630
|
end
|
620
631
|
|
621
|
-
|
622
|
-
|
623
|
-
|
624
|
-
outputter.print_summary(results, elapsed_time)
|
625
|
-
code = results.ok ? 0 : 2
|
626
|
-
end
|
627
|
-
code
|
628
|
-
rescue Bolt::Error => e
|
629
|
-
outputter.fatal_error(e)
|
630
|
-
raise e
|
631
|
-
ensure
|
632
|
-
# restore original signal handler
|
633
|
-
Signal.trap :INT, handler if handler
|
634
|
-
analytics&.finish
|
635
|
-
end
|
636
|
-
|
637
|
-
def show_task(task_name)
|
638
|
-
outputter.print_task_info(pal.get_task(task_name))
|
639
|
-
end
|
640
|
-
|
641
|
-
# Filters a list of content by matching substring.
|
642
|
-
#
|
643
|
-
private def filter_content(content, filter)
|
644
|
-
return content unless content && filter
|
645
|
-
content.select { |name,| name.include?(filter) }
|
646
|
-
end
|
647
|
-
|
648
|
-
def list_tasks
|
649
|
-
tasks = filter_content(pal.list_tasks_with_cache(filter_content: true), options[:filter])
|
650
|
-
outputter.print_tasks(tasks, pal.user_modulepath)
|
651
|
-
end
|
632
|
+
when 'plugin'
|
633
|
+
outputter.print_plugin_list(**app.list_plugins)
|
634
|
+
SUCCESS
|
652
635
|
|
653
|
-
|
654
|
-
|
655
|
-
|
656
|
-
|
657
|
-
|
658
|
-
|
659
|
-
|
660
|
-
|
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
|
661
645
|
|
662
|
-
|
663
|
-
|
664
|
-
|
665
|
-
|
666
|
-
|
667
|
-
|
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
|
668
654
|
|
669
|
-
|
670
|
-
|
671
|
-
|
672
|
-
|
673
|
-
|
674
|
-
|
675
|
-
|
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
|
676
670
|
|
677
|
-
|
678
|
-
|
679
|
-
|
680
|
-
|
681
|
-
|
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
|
682
688
|
end
|
683
|
-
|
684
|
-
outputter.print_target_info(
|
685
|
-
group_targets_by_source,
|
686
|
-
inventory.source,
|
687
|
-
config.default_inventoryfile,
|
688
|
-
target_flag
|
689
|
-
)
|
690
689
|
end
|
691
690
|
|
692
|
-
#
|
693
|
-
#
|
691
|
+
# Process the target list by turning a PuppetDB query or rerun mode into a
|
692
|
+
# list of target names.
|
694
693
|
#
|
695
|
-
|
696
|
-
|
697
|
-
|
698
|
-
|
699
|
-
|
700
|
-
|
701
|
-
|
702
|
-
|
703
|
-
|
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]
|
704
706
|
end
|
705
|
-
|
706
|
-
{ inventory: inventory_targets, adhoc: adhoc_targets }
|
707
|
-
end
|
708
|
-
|
709
|
-
def list_groups
|
710
|
-
outputter.print_groups(inventory.group_names.sort, inventory.source, config.default_inventoryfile)
|
711
707
|
end
|
712
708
|
|
713
|
-
#
|
714
|
-
# provided variable values for interpolations
|
709
|
+
# List content that ships with Bolt.
|
715
710
|
#
|
716
|
-
|
717
|
-
result = pal.plan_hierarchy_lookup(key, plan_vars: plan_vars)
|
718
|
-
outputter.print_plan_lookup(result)
|
719
|
-
0
|
720
|
-
end
|
721
|
-
|
722
|
-
# Looks up a value with Hiera, using targets as the contexts to perform the
|
723
|
-
# look ups in. This should return the same value as a lookup in an apply block.
|
711
|
+
# @param options [Hash] The CLI options.
|
724
712
|
#
|
725
|
-
def
|
726
|
-
|
727
|
-
|
728
|
-
|
729
|
-
|
730
|
-
|
731
|
-
|
732
|
-
|
733
|
-
|
734
|
-
executor.subscribe(outputter) if config.format == 'human'
|
735
|
-
executor.subscribe(log_outputter)
|
736
|
-
executor.publish_event(type: :plan_start, plan: nil)
|
737
|
-
|
738
|
-
results = outputter.spin do
|
739
|
-
pal.lookup(
|
740
|
-
key,
|
741
|
-
targets,
|
742
|
-
inventory,
|
743
|
-
executor,
|
744
|
-
plan_vars: plan_vars
|
745
|
-
)
|
746
|
-
end
|
747
|
-
|
748
|
-
executor.shutdown
|
749
|
-
outputter.print_result_set(results)
|
750
|
-
|
751
|
-
results.ok ? 0 : 1
|
752
|
-
end
|
753
|
-
|
754
|
-
def run_plan(plan_name, plan_arguments, nodes, options)
|
755
|
-
unless nodes.empty?
|
756
|
-
if plan_arguments['nodes'] || plan_arguments['targets']
|
757
|
-
key = plan_arguments.include?('nodes') ? 'nodes' : 'targets'
|
758
|
-
raise Bolt::CLIError,
|
759
|
-
"A plan's '#{key}' parameter can be specified using the --#{key} option, but in that " \
|
760
|
-
"case it must not be specified as a separate #{key}=<value> parameter nor included " \
|
761
|
-
"in the JSON data passed in the --params option"
|
762
|
-
end
|
763
|
-
|
764
|
-
plan_params = pal.get_plan_info(plan_name)['parameters']
|
765
|
-
target_param = plan_params.dig('targets', 'type') =~ /TargetSpec/
|
766
|
-
node_param = plan_params.include?('nodes')
|
767
|
-
|
768
|
-
if node_param && target_param
|
769
|
-
msg = "Plan parameters include both 'nodes' and 'targets' with type 'TargetSpec', " \
|
770
|
-
"neither will populated with the value for --nodes or --targets."
|
771
|
-
Bolt::Logger.warn("nodes_targets_parameters", msg)
|
772
|
-
elsif node_param
|
773
|
-
plan_arguments['nodes'] = nodes.join(',')
|
774
|
-
elsif target_param
|
775
|
-
plan_arguments['targets'] = nodes.join(',')
|
776
|
-
end
|
777
|
-
end
|
778
|
-
|
779
|
-
plan_context = { plan_name: plan_name,
|
780
|
-
params: plan_arguments }
|
781
|
-
|
782
|
-
executor = Bolt::Executor.new(config.concurrency,
|
783
|
-
analytics,
|
784
|
-
options[:noop],
|
785
|
-
config.modified_concurrency,
|
786
|
-
config.future)
|
787
|
-
if %w[human rainbow].include?(options.fetch(:format, 'human'))
|
788
|
-
executor.subscribe(outputter)
|
789
|
-
else
|
790
|
-
# Only subscribe to out module events for JSON outputter
|
791
|
-
executor.subscribe(outputter, %i[message verbose])
|
792
|
-
end
|
793
|
-
|
794
|
-
executor.subscribe(log_outputter)
|
795
|
-
executor.start_plan(plan_context)
|
796
|
-
result = pal.run_plan(plan_name, plan_arguments, executor, inventory, puppetdb_client)
|
797
|
-
|
798
|
-
# If a non-bolt exception bubbles up the plan won't get finished
|
799
|
-
executor.finish_plan(result)
|
800
|
-
executor.shutdown
|
801
|
-
rerun.update(result)
|
802
|
-
|
803
|
-
outputter.print_plan_result(result)
|
804
|
-
result.ok? ? 0 : 1
|
805
|
-
end
|
806
|
-
|
807
|
-
def apply_manifest(code, targets, filename = nil, noop = false)
|
808
|
-
Puppet[:tasks] = false
|
809
|
-
ast = pal.parse_manifest(code, filename)
|
810
|
-
|
811
|
-
if defined?(ast.body) &&
|
812
|
-
(ast.body.is_a?(Puppet::Pops::Model::HostClassDefinition) ||
|
813
|
-
ast.body.is_a?(Puppet::Pops::Model::ResourceTypeDefinition))
|
814
|
-
message = "Manifest only contains definitions and will result in no changes on the targets. "\
|
815
|
-
"Definitions must be declared for their resources to be applied. You can read more "\
|
816
|
-
"about defining and declaring classes and types in the Puppet documentation at "\
|
817
|
-
"https://puppet.com/docs/puppet/latest/lang_classes.html and "\
|
818
|
-
"https://puppet.com/docs/puppet/latest/lang_defined_types.html"
|
819
|
-
Bolt::Logger.warn("empty_manifest", message)
|
820
|
-
end
|
821
|
-
|
822
|
-
executor = Bolt::Executor.new(config.concurrency,
|
823
|
-
analytics,
|
824
|
-
noop,
|
825
|
-
config.modified_concurrency,
|
826
|
-
config.future)
|
827
|
-
executor.subscribe(outputter) if options.fetch(:format, 'human') == 'human'
|
828
|
-
executor.subscribe(log_outputter)
|
829
|
-
# apply logging looks like plan logging, so tell the outputter we're in a
|
830
|
-
# plan even though we're not
|
831
|
-
executor.publish_event(type: :plan_start, plan: nil)
|
832
|
-
|
833
|
-
results = nil
|
834
|
-
elapsed_time = Benchmark.realtime do
|
835
|
-
pal.in_plan_compiler(executor, inventory, puppetdb_client) do |compiler|
|
836
|
-
compiler.call_function('apply_prep', targets)
|
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
|
837
722
|
end
|
838
|
-
|
839
|
-
|
840
|
-
Puppet.lookup(:apply_executor).apply_ast(ast, targets, catch_errors: true, noop: noop)
|
723
|
+
content['Task'] = default_content.list_tasks.each_with_object([]) do |iter, col|
|
724
|
+
col << iter&.first
|
841
725
|
end
|
842
726
|
end
|
843
727
|
|
844
|
-
|
845
|
-
outputter.print_apply_result(results, elapsed_time)
|
846
|
-
rerun.update(results)
|
847
|
-
|
848
|
-
results.ok ? 0 : 1
|
849
|
-
end
|
850
|
-
|
851
|
-
def list_modules
|
852
|
-
outputter.print_module_list(pal.list_modules)
|
853
|
-
end
|
854
|
-
|
855
|
-
def list_plugins
|
856
|
-
outputter.print_plugin_list(plugins.list_plugins, pal.user_modulepath)
|
857
|
-
end
|
858
|
-
|
859
|
-
def generate_types
|
860
|
-
# generate_types will surface a nice error with helpful message if it fails
|
861
|
-
pal.generate_types(cache: true)
|
862
|
-
0
|
728
|
+
content
|
863
729
|
end
|
864
730
|
|
865
|
-
#
|
731
|
+
# Check and warn if Bolt is installed as a gem.
|
866
732
|
#
|
867
|
-
def
|
868
|
-
|
869
|
-
|
870
|
-
|
871
|
-
|
872
|
-
|
873
|
-
"specify any module dependencies. Nothing to do."
|
874
|
-
)
|
875
|
-
return 0
|
876
|
-
end
|
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
|
877
739
|
|
878
|
-
|
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
|
879
743
|
|
880
|
-
|
881
|
-
installer.install(project.modules,
|
882
|
-
project.puppetfile,
|
883
|
-
project.managed_moduledir,
|
884
|
-
config,
|
885
|
-
force: force,
|
886
|
-
resolve: resolve)
|
744
|
+
Bolt::Logger.warn("gem_install", msg)
|
887
745
|
end
|
888
|
-
|
889
|
-
ok ? 0 : 1
|
890
746
|
end
|
891
747
|
|
892
|
-
#
|
748
|
+
# Print a fatal error. Print using the outputter if it's configured.
|
749
|
+
# Otherwise, mock the output by printing directly to stdout.
|
750
|
+
#
|
751
|
+
# @param error [StandardError] The error to print.
|
893
752
|
#
|
894
|
-
def
|
895
|
-
|
896
|
-
|
897
|
-
|
898
|
-
|
899
|
-
|
900
|
-
|
901
|
-
project.modules,
|
902
|
-
project.puppetfile,
|
903
|
-
project.managed_moduledir,
|
904
|
-
project.project_file,
|
905
|
-
config)
|
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)
|
906
760
|
end
|
907
|
-
|
908
|
-
ok ? 0 : 1
|
909
761
|
end
|
910
762
|
|
911
|
-
#
|
763
|
+
# Query whether Bolt is installed as a gem or package by checking if all
|
764
|
+
# built-in modules are installed.
|
912
765
|
#
|
913
|
-
def
|
914
|
-
|
915
|
-
|
916
|
-
|
917
|
-
msg = "Could not find project configuration file #{project.project_file}, unable "\
|
918
|
-
"to install modules. To create a Bolt project, run '#{command}'."
|
919
|
-
|
920
|
-
raise Bolt::Error.new(msg, 'bolt/missing-project-config-error')
|
921
|
-
end
|
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.
|
772
|
+
#
|
773
|
+
# @param options [Hash] Options from the calling method.
|
925
774
|
#
|
926
|
-
def
|
927
|
-
|
928
|
-
|
929
|
-
|
930
|
-
|
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
|
+
{}
|
931
780
|
end
|
932
|
-
|
933
|
-
ok ? 0 : 1
|
934
781
|
end
|
935
782
|
|
936
|
-
|
937
|
-
|
938
|
-
|
939
|
-
|
940
|
-
|
941
|
-
|
942
|
-
|
943
|
-
config.project)
|
783
|
+
# Parse variables for lookups.
|
784
|
+
#
|
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) }]
|
944
790
|
end
|
945
791
|
|
946
|
-
#
|
947
|
-
|
948
|
-
|
949
|
-
|
950
|
-
|
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]}"
|
951
803
|
|
952
|
-
|
953
|
-
|
954
|
-
topic = File.basename(file, '.txt')
|
955
|
-
guides[topic] = File.join(root_path, file)
|
956
|
-
end
|
957
|
-
rescue SystemCallError => e
|
958
|
-
raise Bolt::FileError.new("#{e.message}: unable to load guides directory", root_path)
|
804
|
+
if options[:action] == 'show' && options[:object]
|
805
|
+
screen += '_object'
|
959
806
|
end
|
960
|
-
end
|
961
|
-
|
962
|
-
# Display the list of available Bolt guides.
|
963
|
-
def list_topics
|
964
|
-
outputter.print_topics(guides.keys - ['guide'])
|
965
|
-
0
|
966
|
-
end
|
967
807
|
|
968
|
-
|
969
|
-
|
970
|
-
|
971
|
-
|
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
|
972
817
|
|
973
|
-
|
974
|
-
|
975
|
-
|
976
|
-
|
977
|
-
|
978
|
-
|
979
|
-
outputter.print_guide(guide, topic)
|
980
|
-
else
|
981
|
-
analytics.event('Guide', 'unknown_topic', label: topic)
|
982
|
-
outputter.print_message("Did not find guide for topic '#{topic}'.\n\n")
|
983
|
-
list_topics
|
984
|
-
end
|
985
|
-
0
|
986
|
-
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
|
+
}
|
987
824
|
|
988
|
-
|
989
|
-
|
990
|
-
|
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
|
+
)
|
991
832
|
end
|
992
833
|
|
993
|
-
|
834
|
+
analytics.screen_view(screen, **screen_view_fields)
|
994
835
|
end
|
995
836
|
|
996
|
-
#
|
997
|
-
#
|
998
|
-
# the path is a Puppet file path and looks for the file in a module's files
|
999
|
-
# directory.
|
837
|
+
# Issue a deprecation warning if the user is running an unsupported version
|
838
|
+
# of PowerShell on the controller.
|
1000
839
|
#
|
1001
|
-
def
|
1002
|
-
|
1003
|
-
|
1004
|
-
|
1005
|
-
|
1006
|
-
|
1007
|
-
if modules[mod]
|
1008
|
-
@logger.debug("Did not find file at #{File.expand_path(path)}, checking in module '#{mod}'")
|
1009
|
-
found = Bolt::Util.find_file_in_module(modules[mod].path, file || "", future_file_paths)
|
1010
|
-
path = found.nil? ? File.join(modules[mod].path, 'files', file) : found
|
1011
|
-
end
|
1012
|
-
path
|
1013
|
-
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)
|
1014
845
|
|
1015
|
-
|
1016
|
-
@rerun ||= Bolt::Rerun.new(config.rerunfile, config.save_rerun)
|
1017
|
-
end
|
846
|
+
return unless !stdout.empty? && stdout.to_i < 3
|
1018
847
|
|
1019
|
-
|
1020
|
-
|
1021
|
-
|
1022
|
-
options[:verbose],
|
1023
|
-
config.trace,
|
1024
|
-
config.spinner)
|
848
|
+
msg = "Detected PowerShell 2 on controller. PowerShell 2 is unsupported."
|
849
|
+
Bolt::Logger.deprecation_warning("powershell_2_controller", msg)
|
850
|
+
end
|
1025
851
|
end
|
1026
852
|
|
1027
|
-
|
1028
|
-
|
1029
|
-
|
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
|
1030
866
|
|
1031
|
-
|
1032
|
-
|
1033
|
-
client = Bolt::Analytics.build_client(config.analytics)
|
1034
|
-
client.bundled_content = bundled_content
|
1035
|
-
client
|
867
|
+
inventory_cli_opts = %i[authentication escalation transports].each_with_object([]) do |key, acc|
|
868
|
+
acc.concat(Bolt::BoltOptionParser::OPTIONS[key])
|
1036
869
|
end
|
1037
|
-
end
|
1038
870
|
|
1039
|
-
|
1040
|
-
# If the bundled content directory is empty, Bolt is likely installed as a gem.
|
1041
|
-
if ENV['BOLT_GEM'].nil? && incomplete_install?
|
1042
|
-
msg = <<~MSG.chomp
|
1043
|
-
Bolt might be installed as a gem. To use Bolt reliably and with all of its
|
1044
|
-
dependencies, uninstall the 'bolt' gem and install Bolt as a package:
|
1045
|
-
https://puppet.com/docs/bolt/latest/bolt_installing.html
|
1046
|
-
|
1047
|
-
If you meant to install Bolt as a gem and want to disable this warning,
|
1048
|
-
set the BOLT_GEM environment variable.
|
1049
|
-
MSG
|
871
|
+
inventory_cli_opts.concat(%w[no-host-key-check no-ssl no-ssl-verify no-tty])
|
1050
872
|
|
1051
|
-
|
1052
|
-
end
|
873
|
+
conflicting_options = Set.new(opts.keys.map(&:to_s)).intersection(inventory_cli_opts)
|
1053
874
|
|
1054
|
-
|
1055
|
-
|
1056
|
-
|
1057
|
-
|
1058
|
-
|
1059
|
-
default_content = Bolt::PAL.new(Bolt::Config::Modulepath.new([]), nil, nil)
|
1060
|
-
content['Plan'] = default_content.list_plans.each_with_object([]) do |iter, col|
|
1061
|
-
col << iter&.first
|
1062
|
-
end
|
1063
|
-
content['Task'] = default_content.list_tasks.each_with_object([]) do |iter, col|
|
1064
|
-
col << iter&.first
|
1065
|
-
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
|
+
)
|
1066
880
|
end
|
1067
|
-
|
1068
|
-
content
|
1069
881
|
end
|
1070
882
|
|
1071
|
-
#
|
1072
|
-
#
|
1073
|
-
def
|
1074
|
-
|
1075
|
-
|
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
|
1076
890
|
end
|
1077
891
|
|
1078
|
-
#
|
1079
|
-
#
|
1080
|
-
def
|
1081
|
-
|
1082
|
-
|
1083
|
-
|
1084
|
-
|
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!
|
1085
901
|
end
|
902
|
+
|
903
|
+
yield
|
904
|
+
ensure
|
905
|
+
Signal.trap :INT, handler if handler
|
1086
906
|
end
|
1087
907
|
end
|
1088
908
|
end
|