bolt 0.21.3 → 0.21.4
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/exe/bolt-inventory-pdb +8 -2
- data/lib/bolt/applicator.rb +89 -11
- data/lib/bolt/bolt_option_parser.rb +18 -3
- data/lib/bolt/boltdir.rb +42 -0
- data/lib/bolt/catalog.rb +7 -52
- data/lib/bolt/catalog/compiler.rb +48 -0
- data/lib/bolt/catalog/logging.rb +15 -0
- data/lib/bolt/cli.rb +146 -85
- data/lib/bolt/config.rb +121 -156
- data/lib/bolt/error.rb +25 -2
- data/lib/bolt/executor.rb +3 -4
- data/lib/bolt/inventory.rb +3 -3
- data/lib/bolt/logger.rb +3 -3
- data/lib/bolt/outputter/human.rb +10 -0
- data/lib/bolt/outputter/json.rb +6 -0
- data/lib/bolt/pal.rb +10 -11
- data/lib/bolt/pal/logging.rb +3 -14
- data/lib/bolt/puppetdb/client.rb +7 -27
- data/lib/bolt/puppetdb/config.rb +50 -23
- data/lib/bolt/r10k_log_proxy.rb +30 -0
- data/lib/bolt/util.rb +2 -2
- data/lib/bolt/util/puppet_log_level.rb +20 -0
- data/lib/bolt/version.rb +1 -1
- data/lib/bolt_ext/puppetdb_inventory.rb +3 -9
- data/lib/bolt_spec/plans.rb +174 -0
- data/lib/bolt_spec/plans/mock_executor.rb +217 -0
- metadata +23 -3
- data/lib/bolt/util/on_access.rb +0 -26
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 81d6e90b40ddb51ff8c5b4817ba930417ec7aec118a63e8b585b38d6a5555040
|
4
|
+
data.tar.gz: 3e99e5ba914908058503d1feb9bd8fe5331474288d863ec158a798142b0f41be
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9e7d9c78f78df1dffa0e4288cefa10216182091c16b70ace73b6e1a766a4ebff7fc6cedb403b9794851c844b9050c63a13f1beb137a87e5f53af2c9b2de7af54
|
7
|
+
data.tar.gz: 1edc8a0cb84632e703a9c2b0e50d6478cdbb2e5a56e0bb7bc06e9395e6dba1bcbc7232423eca87afef58f904c5922af782041079070b819f08d5228dbb3c029b
|
data/exe/bolt-inventory-pdb
CHANGED
@@ -3,5 +3,11 @@
|
|
3
3
|
|
4
4
|
require 'bolt_ext/puppetdb_inventory'
|
5
5
|
|
6
|
-
|
7
|
-
|
6
|
+
begin
|
7
|
+
Bolt::PuppetDBInventory::CLI.new(ARGV).run
|
8
|
+
exit 0
|
9
|
+
rescue StandardError => e
|
10
|
+
warn "Error: #{e}"
|
11
|
+
warn e.backtrace if @trace
|
12
|
+
exit 1
|
13
|
+
end
|
data/lib/bolt/applicator.rb
CHANGED
@@ -1,21 +1,24 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'json'
|
4
|
+
require 'logging'
|
4
5
|
require 'open3'
|
5
6
|
require 'concurrent'
|
7
|
+
require 'bolt/util/puppet_log_level'
|
6
8
|
|
7
9
|
module Bolt
|
8
10
|
Task = Struct.new(:name, :implementations, :input_method)
|
9
11
|
|
10
12
|
class Applicator
|
11
|
-
def initialize(inventory, executor, modulepath,
|
13
|
+
def initialize(inventory, executor, modulepath, pdb_client, hiera_config, max_compiles)
|
12
14
|
@inventory = inventory
|
13
15
|
@executor = executor
|
14
16
|
@modulepath = modulepath
|
15
|
-
@
|
17
|
+
@pdb_client = pdb_client
|
16
18
|
@hiera_config = hiera_config ? validate_hiera_config(hiera_config) : nil
|
17
19
|
|
18
20
|
@pool = Concurrent::ThreadPoolExecutor.new(max_threads: max_compiles)
|
21
|
+
@logger = Logging.logger[self]
|
19
22
|
end
|
20
23
|
|
21
24
|
private def libexec
|
@@ -36,7 +39,7 @@ module Bolt
|
|
36
39
|
catalog_input = {
|
37
40
|
code_ast: ast,
|
38
41
|
modulepath: @modulepath,
|
39
|
-
pdb_config: @
|
42
|
+
pdb_config: @pdb_client.config.to_hash,
|
40
43
|
hiera_config: @hiera_config,
|
41
44
|
target: {
|
42
45
|
name: target.host,
|
@@ -53,7 +56,28 @@ module Bolt
|
|
53
56
|
out, err, stat = Open3.capture3('ruby', bolt_catalog_exe, 'compile', stdin_data: catalog_input.to_json)
|
54
57
|
ENV['PATH'] = old_path
|
55
58
|
|
56
|
-
|
59
|
+
# stderr may contain formatted logs from Puppet's logger or other errors.
|
60
|
+
# Print them in order, but handle them separately. Anything not a formatted log is assumed
|
61
|
+
# to be an error message.
|
62
|
+
logs = err.lines.map do |l|
|
63
|
+
begin
|
64
|
+
JSON.parse(l)
|
65
|
+
rescue StandardError
|
66
|
+
l
|
67
|
+
end
|
68
|
+
end
|
69
|
+
logs.each do |log|
|
70
|
+
if log.is_a?(String)
|
71
|
+
@logger.error(log.chomp)
|
72
|
+
else
|
73
|
+
log.map { |k, v| [k.to_sym, v] }.each do |level, msg|
|
74
|
+
bolt_level = Bolt::Util::PuppetLogLevel::MAPPING[level]
|
75
|
+
@logger.send(bolt_level, "#{target.name}: #{msg.chomp}")
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
raise(ApplyError, target.name) unless stat.success?
|
57
81
|
JSON.parse(out)
|
58
82
|
end
|
59
83
|
|
@@ -61,22 +85,70 @@ module Bolt
|
|
61
85
|
if File.exist?(File.path(hiera_config))
|
62
86
|
data = File.open(File.path(hiera_config), "r:UTF-8") { |f| YAML.safe_load(f.read) }
|
63
87
|
unless data['version'] == 5
|
64
|
-
raise
|
88
|
+
raise Bolt::ParseError, "Hiera v5 is required, found v#{data['version'] || 3} in #{hiera_config}"
|
65
89
|
end
|
66
90
|
hiera_config
|
67
91
|
end
|
68
92
|
end
|
69
93
|
|
94
|
+
def provide_puppet_missing_errors(result)
|
95
|
+
error_hash = result.error_hash
|
96
|
+
exit_code = error_hash['details']['exit_code'] if error_hash && error_hash['details']
|
97
|
+
# If we get exit code 126 or 127 back, it means the shebang command wasn't found; Puppet isn't present
|
98
|
+
if [126, 127].include?(exit_code)
|
99
|
+
Result.new(result.target, error:
|
100
|
+
{
|
101
|
+
'msg' => "Puppet is not installed on the target, please install it to enable 'apply'",
|
102
|
+
'kind' => 'bolt/apply-error'
|
103
|
+
})
|
104
|
+
elsif exit_code == 1 && error_hash['msg'] =~ /Could not find executable 'ruby.exe'/
|
105
|
+
# Windows does not have Ruby present
|
106
|
+
Result.new(result.target, error:
|
107
|
+
{
|
108
|
+
'msg' => "Puppet is not installed on the target in $env:ProgramFiles, please install it to enable 'apply'",
|
109
|
+
'kind' => 'bolt/apply-error'
|
110
|
+
})
|
111
|
+
elsif exit_code == 1 && error_hash['msg'] =~ /cannot load such file -- puppet \(LoadError\)/
|
112
|
+
# Windows uses a Ruby that doesn't have Puppet installed
|
113
|
+
# TODO: fix so we don't find other Rubies, or point to a known issues URL for more info
|
114
|
+
Result.new(result.target, error:
|
115
|
+
{
|
116
|
+
'msg' => 'Found a Ruby without Puppet present, please install Puppet ' \
|
117
|
+
"or remove Ruby from $env:Path to enable 'apply'",
|
118
|
+
'kind' => 'bolt/apply-error'
|
119
|
+
})
|
120
|
+
else
|
121
|
+
result
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
def identify_resource_failures(result)
|
126
|
+
if result.ok? && result.value['status'] == 'failed'
|
127
|
+
resources = result.value['resource_statuses']
|
128
|
+
failed = resources.select { |_, r| r['failed'] }.flat_map do |key, resource|
|
129
|
+
resource['events'].select { |e| e['status'] == 'failure' }.map do |event|
|
130
|
+
"\n #{key}: #{event['message']}"
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
result.value['_error'] = {
|
135
|
+
'msg' => "Resources failed to apply for #{result.target.name}#{failed.join}",
|
136
|
+
'kind' => 'bolt/resource-failure'
|
137
|
+
}
|
138
|
+
end
|
139
|
+
result
|
140
|
+
end
|
141
|
+
|
70
142
|
def apply(args, apply_body, scope)
|
71
143
|
raise(ArgumentError, 'apply requires a TargetSpec') if args.empty?
|
72
144
|
type0 = Puppet.lookup(:pal_script_compiler).type('TargetSpec')
|
73
145
|
Puppet::Pal.assert_type(type0, args[0], 'apply targets')
|
74
146
|
|
75
|
-
|
147
|
+
options = {}
|
76
148
|
if args.count > 1
|
77
149
|
type1 = Puppet.lookup(:pal_script_compiler).type('Hash[String, Data]')
|
78
150
|
Puppet::Pal.assert_type(type1, args[1], 'apply options')
|
79
|
-
|
151
|
+
options = args[1]
|
80
152
|
end
|
81
153
|
|
82
154
|
# collect plan vars and merge them over target vars
|
@@ -87,7 +159,7 @@ module Bolt
|
|
87
159
|
ast = Puppet::Pops::Serialization::ToDataConverter.convert(apply_body, rich_data: true, symbol_to_string: true)
|
88
160
|
notify = proc { |_| nil }
|
89
161
|
|
90
|
-
@executor.log_action('apply catalog', targets) do
|
162
|
+
r = @executor.log_action('apply catalog', targets) do
|
91
163
|
futures = targets.map do |target|
|
92
164
|
Concurrent::Future.execute(executor: @pool) do
|
93
165
|
@executor.with_node_logging("Compiling manifest block", [target]) do
|
@@ -99,16 +171,22 @@ module Bolt
|
|
99
171
|
result_promises = targets.zip(futures).flat_map do |target, future|
|
100
172
|
@executor.queue_execute([target]) do |transport, batch|
|
101
173
|
@executor.with_node_logging("Applying manifest block", batch) do
|
102
|
-
arguments =
|
103
|
-
arguments['catalog'] = future.value
|
174
|
+
arguments = { 'catalog' => future.value, '_noop' => options['_noop'] }
|
104
175
|
raise future.reason if future.rejected?
|
105
|
-
transport.batch_task(batch, catalog_apply_task, arguments,
|
176
|
+
result = transport.batch_task(batch, catalog_apply_task, arguments, options, ¬ify)
|
177
|
+
result = provide_puppet_missing_errors(result)
|
178
|
+
identify_resource_failures(result)
|
106
179
|
end
|
107
180
|
end
|
108
181
|
end
|
109
182
|
|
110
183
|
@executor.await_results(result_promises)
|
111
184
|
end
|
185
|
+
|
186
|
+
if !r.ok && !options['_catch_errors']
|
187
|
+
raise Bolt::ApplyFailure, r
|
188
|
+
end
|
189
|
+
r
|
112
190
|
end
|
113
191
|
end
|
114
192
|
end
|
@@ -28,6 +28,7 @@ Available subcommands:
|
|
28
28
|
bolt plan show Show list of available plans
|
29
29
|
bolt plan show <plan> Show details for plan
|
30
30
|
bolt plan run <plan> [params] Run a Puppet task plan
|
31
|
+
bolt puppetfile install Install modules from a Puppetfile into a Boltdir
|
31
32
|
|
32
33
|
Run `bolt <subcommand> --help` to view specific examples.
|
33
34
|
|
@@ -89,6 +90,18 @@ Available actions are:
|
|
89
90
|
upload <src> <dest> Upload local file <src> to <dest> on each node
|
90
91
|
|
91
92
|
#{examples('file upload /tmp/source /etc/profile.d/login.sh', 'upload a file to')}
|
93
|
+
Available options are:
|
94
|
+
HELP
|
95
|
+
|
96
|
+
PUPPETFILE_HELP = <<-HELP
|
97
|
+
Usage: bolt puppetfile <action> [options]
|
98
|
+
|
99
|
+
Available actions are:
|
100
|
+
install Install modules from a Puppetfile into a Boltdir
|
101
|
+
|
102
|
+
Install modules into the local Boltdir
|
103
|
+
bolt puppetfile install
|
104
|
+
|
92
105
|
Available options are:
|
93
106
|
HELP
|
94
107
|
|
@@ -193,7 +206,7 @@ Available options are:
|
|
193
206
|
end
|
194
207
|
define('--boltdir FILEPATH',
|
195
208
|
'Specify what Boltdir to load config from (default: autodiscovered from current working dir)') do |path|
|
196
|
-
@options[:
|
209
|
+
@options[:boltdir] = path
|
197
210
|
end
|
198
211
|
define('--configfile FILEPATH',
|
199
212
|
'Specify where to load config from (default: ~/.puppetlabs/bolt/bolt.yaml)') do |path|
|
@@ -253,8 +266,8 @@ Available options are:
|
|
253
266
|
# show the --nodes and --query switches by default
|
254
267
|
@nodes.hide = @query.hide = false
|
255
268
|
|
256
|
-
# Update the banner according to the
|
257
|
-
self.banner = case @options[:
|
269
|
+
# Update the banner according to the subcommand
|
270
|
+
self.banner = case @options[:subcommand]
|
258
271
|
when 'plan'
|
259
272
|
# don't show the --nodes and --query switches in the plan help
|
260
273
|
@nodes.hide = @query.hide = true
|
@@ -267,6 +280,8 @@ Available options are:
|
|
267
280
|
TASK_HELP
|
268
281
|
when 'file'
|
269
282
|
FILE_HELP
|
283
|
+
when 'puppetfile'
|
284
|
+
PUPPETFILE_HELP
|
270
285
|
else
|
271
286
|
BANNER
|
272
287
|
end
|
data/lib/bolt/boltdir.rb
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'pathname'
|
4
|
+
|
5
|
+
module Bolt
|
6
|
+
class Boltdir
|
7
|
+
BOLTDIR_NAME = 'Boltdir'
|
8
|
+
|
9
|
+
attr_reader :path, :config_file, :inventory_file, :modulepath, :hiera_config, :puppetfile
|
10
|
+
|
11
|
+
def self.default_boltdir
|
12
|
+
Boltdir.new(File.join('~', '.puppetlabs', 'bolt'))
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.find_boltdir(dir)
|
16
|
+
local_boltdir = Pathname.new(dir).ascend do |path|
|
17
|
+
boltdir = path + BOLTDIR_NAME
|
18
|
+
break new(boltdir) if boltdir.directory?
|
19
|
+
end
|
20
|
+
|
21
|
+
local_boltdir || default_boltdir
|
22
|
+
end
|
23
|
+
|
24
|
+
def initialize(path)
|
25
|
+
@path = Pathname.new(path).expand_path
|
26
|
+
@config_file = @path + 'bolt.yaml'
|
27
|
+
@inventory_file = @path + 'inventory.yaml'
|
28
|
+
@modulepath = [(@path + 'modules').to_s]
|
29
|
+
@hiera_config = @path + 'hiera.yaml'
|
30
|
+
@puppetfile = @path + 'Puppetfile'
|
31
|
+
end
|
32
|
+
|
33
|
+
def to_s
|
34
|
+
@path.to_s
|
35
|
+
end
|
36
|
+
|
37
|
+
def eql?(other)
|
38
|
+
path == other.path
|
39
|
+
end
|
40
|
+
alias == eql?
|
41
|
+
end
|
42
|
+
end
|
data/lib/bolt/catalog.rb
CHANGED
@@ -2,56 +2,11 @@
|
|
2
2
|
|
3
3
|
require 'bolt/pal'
|
4
4
|
require 'bolt/puppetdb'
|
5
|
-
require 'bolt/util/on_access'
|
6
5
|
|
7
6
|
Bolt::PAL.load_puppet
|
8
7
|
|
9
|
-
|
10
|
-
|
11
|
-
# node environment so our AST is in the '' hostclass instead of doing it here.
|
12
|
-
module Puppet
|
13
|
-
module Parser
|
14
|
-
class BoltCompiler < Puppet::Parser::Compiler
|
15
|
-
def internal_evaluator
|
16
|
-
@internal_evaluator ||= Puppet::Pops::Parser::EvaluatingParser.new
|
17
|
-
end
|
18
|
-
|
19
|
-
def dump_ast(ast)
|
20
|
-
Puppet::Pops::Serialization::ToDataConverter.convert(ast, rich_data: true, symbol_to_string: true)
|
21
|
-
end
|
22
|
-
|
23
|
-
def load_ast(ast_data)
|
24
|
-
Puppet::Pops::Serialization::FromDataConverter.convert(ast_data)
|
25
|
-
end
|
26
|
-
|
27
|
-
def parse_string(string, file = '')
|
28
|
-
internal_evaluator.parse_string(string, file)
|
29
|
-
end
|
30
|
-
|
31
|
-
def evaluate_main
|
32
|
-
main = Puppet.lookup(:pal_main)
|
33
|
-
ast = if main.is_a?(String)
|
34
|
-
parse_string(main)
|
35
|
-
else
|
36
|
-
load_ast(main)
|
37
|
-
end
|
38
|
-
|
39
|
-
bridge = Puppet::Parser::AST::PopsBridge::Program.new(ast)
|
40
|
-
|
41
|
-
# This is more or less copypaste from the super but we don't use the
|
42
|
-
# original host_class.
|
43
|
-
krt = environment.known_resource_types
|
44
|
-
@main = krt.add(Puppet::Resource::Type.new(:hostclass, '', code: bridge))
|
45
|
-
@topscope.source = @main
|
46
|
-
@main_resource = Puppet::Parser::Resource.new('class', :main, scope: @topscope, source: @main)
|
47
|
-
@topscope.resource = @main_resource
|
48
|
-
add_resource(@topscope, @main_resource)
|
49
|
-
|
50
|
-
@main_resource.evaluate
|
51
|
-
end
|
52
|
-
end
|
53
|
-
end
|
54
|
-
end
|
8
|
+
require 'bolt/catalog/compiler'
|
9
|
+
require 'bolt/catalog/logging'
|
55
10
|
|
56
11
|
module Bolt
|
57
12
|
class Catalog
|
@@ -64,7 +19,10 @@ module Bolt
|
|
64
19
|
Puppet.settings.send(:clear_everything_for_tests)
|
65
20
|
Puppet.initialize_settings(cli)
|
66
21
|
Puppet.settings[:hiera_config] = hiera_config
|
67
|
-
|
22
|
+
|
23
|
+
# Use a special logdest that serializes all log messages and their level to stderr.
|
24
|
+
Puppet::Util::Log.newdestination(:stderr)
|
25
|
+
Puppet.settings[:log_level] = 'debug'
|
68
26
|
yield
|
69
27
|
end
|
70
28
|
end
|
@@ -97,10 +55,7 @@ module Bolt
|
|
97
55
|
pal_main = request['code_ast'] || request['code_string']
|
98
56
|
target = request['target']
|
99
57
|
|
100
|
-
pdb_client = Bolt::
|
101
|
-
pdb_config = Bolt::PuppetDB::Config.new(nil, request['pdb_config'])
|
102
|
-
Bolt::PuppetDB::Client.from_config(pdb_config)
|
103
|
-
end
|
58
|
+
pdb_client = Bolt::PuppetDB::Client.new(Bolt::PuppetDB::Config.new(request['pdb_config']))
|
104
59
|
|
105
60
|
with_puppet_settings(request['hiera_config']) do
|
106
61
|
Puppet[:code] = ''
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# This class exists to override evaluate_main and let us inject
|
4
|
+
# AST instead of looking for the main manifest. A better option may be to set up the
|
5
|
+
# node environment so our AST is in the '' hostclass instead of doing it here.
|
6
|
+
module Puppet
|
7
|
+
module Parser
|
8
|
+
class BoltCompiler < Puppet::Parser::Compiler
|
9
|
+
def internal_evaluator
|
10
|
+
@internal_evaluator ||= Puppet::Pops::Parser::EvaluatingParser.new
|
11
|
+
end
|
12
|
+
|
13
|
+
def dump_ast(ast)
|
14
|
+
Puppet::Pops::Serialization::ToDataConverter.convert(ast, rich_data: true, symbol_to_string: true)
|
15
|
+
end
|
16
|
+
|
17
|
+
def load_ast(ast_data)
|
18
|
+
Puppet::Pops::Serialization::FromDataConverter.convert(ast_data)
|
19
|
+
end
|
20
|
+
|
21
|
+
def parse_string(string, file = '')
|
22
|
+
internal_evaluator.parse_string(string, file)
|
23
|
+
end
|
24
|
+
|
25
|
+
def evaluate_main
|
26
|
+
main = Puppet.lookup(:pal_main)
|
27
|
+
ast = if main.is_a?(String)
|
28
|
+
parse_string(main)
|
29
|
+
else
|
30
|
+
load_ast(main)
|
31
|
+
end
|
32
|
+
|
33
|
+
bridge = Puppet::Parser::AST::PopsBridge::Program.new(ast)
|
34
|
+
|
35
|
+
# This is more or less copypaste from the super but we don't use the
|
36
|
+
# original host_class.
|
37
|
+
krt = environment.known_resource_types
|
38
|
+
@main = krt.add(Puppet::Resource::Type.new(:hostclass, '', code: bridge))
|
39
|
+
@topscope.source = @main
|
40
|
+
@main_resource = Puppet::Parser::Resource.new('class', :main, scope: @topscope, source: @main)
|
41
|
+
@topscope.resource = @main_resource
|
42
|
+
add_resource(@topscope, @main_resource)
|
43
|
+
|
44
|
+
@main_resource.evaluate
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
Puppet::Util::Log.newdesttype :stderr do
|
4
|
+
def initialize
|
5
|
+
# Flush output immediately.
|
6
|
+
$stderr.sync = true
|
7
|
+
end
|
8
|
+
|
9
|
+
# Emits message as a single line of JSON mapping level to message string.
|
10
|
+
def handle(msg)
|
11
|
+
str = msg.respond_to?(:multiline) ? msg.multiline : msg.to_s
|
12
|
+
str = msg.source == "Puppet" ? str : "#{msg.source}: #{str}"
|
13
|
+
warn({ msg.level => str }.to_json)
|
14
|
+
end
|
15
|
+
end
|
data/lib/bolt/cli.rb
CHANGED
@@ -6,6 +6,7 @@ require 'json'
|
|
6
6
|
require 'io/console'
|
7
7
|
require 'logging'
|
8
8
|
require 'optparse'
|
9
|
+
require 'r10k/action/puppetfile/install'
|
9
10
|
require 'bolt/analytics'
|
10
11
|
require 'bolt/bolt_option_parser'
|
11
12
|
require 'bolt/config'
|
@@ -16,26 +17,27 @@ require 'bolt/logger'
|
|
16
17
|
require 'bolt/outputter'
|
17
18
|
require 'bolt/puppetdb'
|
18
19
|
require 'bolt/pal'
|
20
|
+
require 'bolt/r10k_log_proxy'
|
19
21
|
require 'bolt/target'
|
20
22
|
require 'bolt/version'
|
21
|
-
require 'bolt/util/on_access'
|
22
23
|
|
23
24
|
module Bolt
|
24
25
|
class CLIExit < StandardError; end
|
25
26
|
class CLI
|
26
|
-
COMMANDS = { 'command'
|
27
|
-
'script'
|
28
|
-
'task'
|
29
|
-
'plan'
|
30
|
-
'file'
|
27
|
+
COMMANDS = { 'command' => %w[run],
|
28
|
+
'script' => %w[run],
|
29
|
+
'task' => %w[show run],
|
30
|
+
'plan' => %w[show run],
|
31
|
+
'file' => %w[upload],
|
32
|
+
'puppetfile' => %w[install] }.freeze
|
31
33
|
|
32
34
|
attr_reader :config, :options
|
33
35
|
|
34
36
|
def initialize(argv)
|
35
37
|
Bolt::Logger.initialize_logging
|
36
38
|
@logger = Logging.logger[self]
|
37
|
-
@config = Bolt::Config.new
|
38
39
|
@argv = argv
|
40
|
+
@config = Bolt::Config.default
|
39
41
|
@options = {
|
40
42
|
nodes: []
|
41
43
|
}
|
@@ -48,15 +50,15 @@ module Bolt
|
|
48
50
|
private :inventory
|
49
51
|
|
50
52
|
def help?(parser, remaining)
|
51
|
-
# Set the
|
52
|
-
options[:
|
53
|
+
# Set the subcommand
|
54
|
+
options[:subcommand] = remaining.shift
|
53
55
|
|
54
|
-
if options[:
|
56
|
+
if options[:subcommand] == 'help'
|
55
57
|
options[:help] = true
|
56
|
-
options[:
|
58
|
+
options[:subcommand] = remaining.shift
|
57
59
|
end
|
58
60
|
|
59
|
-
# Update the parser for the new
|
61
|
+
# Update the parser for the new subcommand
|
60
62
|
parser.update
|
61
63
|
|
62
64
|
options[:help]
|
@@ -73,12 +75,8 @@ module Bolt
|
|
73
75
|
raise Bolt::CLIExit
|
74
76
|
end
|
75
77
|
|
76
|
-
config.update(options)
|
77
|
-
config.validate
|
78
|
-
Bolt::Logger.configure(config)
|
79
|
-
|
80
78
|
# This section handles parsing non-flag options which are
|
81
|
-
#
|
79
|
+
# subcommand specific rather then part of the config
|
82
80
|
options[:action] = remaining.shift
|
83
81
|
options[:object] = remaining.shift
|
84
82
|
|
@@ -99,15 +97,28 @@ module Bolt
|
|
99
97
|
|
100
98
|
validate(options)
|
101
99
|
|
100
|
+
@config = if options[:configfile]
|
101
|
+
Bolt::Config.from_file(options[:configfile], options)
|
102
|
+
else
|
103
|
+
boltdir = if options[:boltdir]
|
104
|
+
Bolt::Boltdir.new(options[:boltdir])
|
105
|
+
else
|
106
|
+
Bolt::Boltdir.find_boltdir(Dir.pwd)
|
107
|
+
end
|
108
|
+
Bolt::Config.from_boltdir(boltdir, options)
|
109
|
+
end
|
110
|
+
|
111
|
+
Bolt::Logger.configure(config.log, config.color)
|
112
|
+
|
102
113
|
# After validation, initialize inventory and targets. Errors here are better to catch early.
|
103
|
-
unless options[:action] == 'show'
|
114
|
+
unless options[:subcommand] == 'puppetfile' || options[:action] == 'show'
|
104
115
|
if options[:query]
|
105
116
|
if options[:nodes].any?
|
106
117
|
raise Bolt::CLIError, "Only one of '--nodes' or '--query' may be specified"
|
107
118
|
end
|
108
119
|
nodes = query_puppetdb_nodes(options[:query])
|
109
120
|
options[:targets] = inventory.get_targets(nodes)
|
110
|
-
options[:nodes] = nodes if options[:
|
121
|
+
options[:nodes] = nodes if options[:subcommand] == 'plan'
|
111
122
|
else
|
112
123
|
options[:targets] = inventory.get_targets(options[:nodes])
|
113
124
|
end
|
@@ -120,42 +131,42 @@ module Bolt
|
|
120
131
|
end
|
121
132
|
|
122
133
|
def validate(options)
|
123
|
-
unless COMMANDS.include?(options[:
|
134
|
+
unless COMMANDS.include?(options[:subcommand])
|
124
135
|
raise Bolt::CLIError,
|
125
|
-
"Expected subcommand '#{options[:
|
136
|
+
"Expected subcommand '#{options[:subcommand]}' to be one of " \
|
126
137
|
"#{COMMANDS.keys.join(', ')}"
|
127
138
|
end
|
128
139
|
|
129
140
|
if options[:action].nil?
|
130
141
|
raise Bolt::CLIError,
|
131
|
-
"Expected an action of the form 'bolt #{options[:
|
142
|
+
"Expected an action of the form 'bolt #{options[:subcommand]} <action>'"
|
132
143
|
end
|
133
144
|
|
134
|
-
actions = COMMANDS[options[:
|
145
|
+
actions = COMMANDS[options[:subcommand]]
|
135
146
|
unless actions.include?(options[:action])
|
136
147
|
raise Bolt::CLIError,
|
137
148
|
"Expected action '#{options[:action]}' to be one of " \
|
138
149
|
"#{actions.join(', ')}"
|
139
150
|
end
|
140
151
|
|
141
|
-
if options[:
|
152
|
+
if options[:subcommand] != 'file' && options[:subcommand] != 'script' &&
|
142
153
|
!options[:leftovers].empty?
|
143
154
|
raise Bolt::CLIError,
|
144
155
|
"Unknown argument(s) #{options[:leftovers].join(', ')}"
|
145
156
|
end
|
146
157
|
|
147
|
-
if %w[task plan].include?(options[:
|
158
|
+
if %w[task plan].include?(options[:subcommand]) && options[:action] == 'run'
|
148
159
|
if options[:object].nil?
|
149
|
-
raise Bolt::CLIError, "Must specify a #{options[:
|
160
|
+
raise Bolt::CLIError, "Must specify a #{options[:subcommand]} to run"
|
150
161
|
end
|
151
162
|
# This may mean that we parsed a parameter as the object
|
152
163
|
unless options[:object] =~ /\A([a-z][a-z0-9_]*)?(::[a-z][a-z0-9_]*)*\Z/
|
153
164
|
raise Bolt::CLIError,
|
154
|
-
"Invalid #{options[:
|
165
|
+
"Invalid #{options[:subcommand]} '#{options[:object]}'"
|
155
166
|
end
|
156
167
|
end
|
157
168
|
|
158
|
-
if options[:
|
169
|
+
if !%w[plan puppetfile].include?(options[:subcommand]) && options[:action] != 'show'
|
159
170
|
if options[:nodes].empty? && options[:query].nil?
|
160
171
|
raise Bolt::CLIError, "Targets must be specified with '--nodes' or '--query'"
|
161
172
|
elsif options[:nodes].any? && options[:query]
|
@@ -163,7 +174,11 @@ module Bolt
|
|
163
174
|
end
|
164
175
|
end
|
165
176
|
|
166
|
-
if options[:
|
177
|
+
if options[:boltdir] && options[:configfile]
|
178
|
+
raise Bolt::CLIError, "Only one of '--boltdir' or '--configfile' may be specified"
|
179
|
+
end
|
180
|
+
|
181
|
+
if options[:noop] && (options[:subcommand] != 'task' || options[:action] != 'run')
|
167
182
|
raise Bolt::CLIError,
|
168
183
|
"Option '--noop' may only be specified when running a task"
|
169
184
|
end
|
@@ -181,10 +196,8 @@ module Bolt
|
|
181
196
|
|
182
197
|
def puppetdb_client
|
183
198
|
return @puppetdb_client if @puppetdb_client
|
184
|
-
|
185
|
-
|
186
|
-
Bolt::PuppetDB::Client.from_config(puppetdb_config)
|
187
|
-
end
|
199
|
+
puppetdb_config = Bolt::PuppetDB::Config.load_config(nil, config.puppetdb)
|
200
|
+
@puppetdb_client = Bolt::PuppetDB::Client.new(puppetdb_config)
|
188
201
|
end
|
189
202
|
|
190
203
|
def query_puppetdb_nodes(query)
|
@@ -203,38 +216,30 @@ module Bolt
|
|
203
216
|
|
204
217
|
@analytics = Bolt::Analytics.build_client
|
205
218
|
|
206
|
-
screen = "#{options[:
|
219
|
+
screen = "#{options[:subcommand]}_#{options[:action]}"
|
207
220
|
# submit a different screen for `bolt task show` and `bolt task show foo`
|
208
221
|
if options[:action] == 'show' && options[:object]
|
209
222
|
screen += '_object'
|
210
223
|
end
|
211
224
|
|
212
225
|
@analytics.screen_view(screen,
|
213
|
-
output_format: config
|
226
|
+
output_format: config.format,
|
214
227
|
target_nodes: options.fetch(:targets, []).count,
|
215
228
|
inventory_nodes: inventory.node_names.count,
|
216
229
|
inventory_groups: inventory.group_names.count)
|
217
230
|
|
218
|
-
if options[:mode] == 'plan' || options[:mode] == 'task'
|
219
|
-
pal = Bolt::PAL.new(config)
|
220
|
-
end
|
221
|
-
|
222
231
|
if options[:action] == 'show'
|
223
|
-
if options[:
|
232
|
+
if options[:subcommand] == 'task'
|
224
233
|
if options[:object]
|
225
|
-
|
234
|
+
show_task(options[:object])
|
226
235
|
else
|
227
|
-
|
228
|
-
outputter.print_message("\nUse `bolt task show <task-name>` to view "\
|
229
|
-
"details and parameters for a specific task.")
|
236
|
+
list_tasks
|
230
237
|
end
|
231
|
-
elsif options[:
|
238
|
+
elsif options[:subcommand] == 'plan'
|
232
239
|
if options[:object]
|
233
|
-
|
240
|
+
show_plan(options[:object])
|
234
241
|
else
|
235
|
-
|
236
|
-
outputter.print_message("\nUse `bolt plan show <plan-name>` to view "\
|
237
|
-
"details and parameters for a specific plan.")
|
242
|
+
list_plans
|
238
243
|
end
|
239
244
|
end
|
240
245
|
return 0
|
@@ -242,36 +247,16 @@ module Bolt
|
|
242
247
|
|
243
248
|
message = 'There may be processes left executing on some nodes.'
|
244
249
|
|
245
|
-
if options[:task_options] && !options[:params_parsed] && pal
|
246
|
-
options[:task_options] = pal.parse_params(options[:
|
250
|
+
if %w[task plan].include?(options[:subcommand]) && options[:task_options] && !options[:params_parsed] && pal
|
251
|
+
options[:task_options] = pal.parse_params(options[:subcommand], options[:object], options[:task_options])
|
247
252
|
end
|
248
253
|
|
249
|
-
if options[:
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
"A plan's 'nodes' parameter may be specified using the --nodes option, but in that " \
|
254
|
-
"case it must not be specified as a separate nodes=<value> parameter nor included " \
|
255
|
-
"in the JSON data passed in the --params option"
|
256
|
-
end
|
257
|
-
options[:task_options]['nodes'] = options[:nodes].join(',')
|
258
|
-
end
|
259
|
-
|
260
|
-
params = options[:noop] ? options[:task_options].merge("_noop" => true) : options[:task_options]
|
261
|
-
plan_context = { plan_name: options[:object],
|
262
|
-
params: params }
|
263
|
-
plan_context[:description] = options[:description] if options[:description]
|
264
|
-
|
265
|
-
executor = Bolt::Executor.new(config, @analytics, options[:noop], bundled_content: bundled_content)
|
266
|
-
executor.start_plan(plan_context)
|
267
|
-
result = pal.run_plan(options[:object], options[:task_options], executor, inventory, puppetdb_client)
|
268
|
-
|
269
|
-
# If a non-bolt exeception bubbles up the plan won't get finished
|
270
|
-
executor.finish_plan(result)
|
271
|
-
outputter.print_plan_result(result)
|
272
|
-
code = result.ok? ? 0 : 1
|
254
|
+
if options[:subcommand] == 'plan'
|
255
|
+
code = run_plan(options[:object], options[:task_options], options[:nodes], options)
|
256
|
+
elsif options[:subcommand] == 'puppetfile'
|
257
|
+
code = install_puppetfile(@config.puppetfile, @config.modulepath)
|
273
258
|
else
|
274
|
-
executor = Bolt::Executor.new(config, @analytics, options[:noop], bundled_content: bundled_content)
|
259
|
+
executor = Bolt::Executor.new(config.concurrency, @analytics, options[:noop], bundled_content: bundled_content)
|
275
260
|
targets = options[:targets]
|
276
261
|
|
277
262
|
results = nil
|
@@ -281,7 +266,7 @@ module Bolt
|
|
281
266
|
executor_opts = {}
|
282
267
|
executor_opts['_description'] = options[:description] if options.key?(:description)
|
283
268
|
results =
|
284
|
-
case options[:
|
269
|
+
case options[:subcommand]
|
285
270
|
when 'command'
|
286
271
|
executor.run_command(targets, options[:object], executor_opts) do |event|
|
287
272
|
outputter.print_event(event)
|
@@ -330,6 +315,80 @@ module Bolt
|
|
330
315
|
@analytics&.finish
|
331
316
|
end
|
332
317
|
|
318
|
+
def show_task(task_name)
|
319
|
+
outputter.print_task_info(pal.get_task_info(task_name))
|
320
|
+
end
|
321
|
+
|
322
|
+
def list_tasks
|
323
|
+
outputter.print_table(pal.list_tasks)
|
324
|
+
outputter.print_message("\nUse `bolt task show <task-name>` to view "\
|
325
|
+
"details and parameters for a specific task.")
|
326
|
+
end
|
327
|
+
|
328
|
+
def show_plan(plan_name)
|
329
|
+
outputter.print_plan_info(pal.get_plan_info(plan_name))
|
330
|
+
end
|
331
|
+
|
332
|
+
def list_plans
|
333
|
+
outputter.print_table(pal.list_plans)
|
334
|
+
outputter.print_message("\nUse `bolt plan show <plan-name>` to view "\
|
335
|
+
"details and parameters for a specific plan.")
|
336
|
+
end
|
337
|
+
|
338
|
+
def run_plan(plan_name, plan_arguments, nodes, options)
|
339
|
+
unless nodes.empty?
|
340
|
+
if plan_arguments['nodes']
|
341
|
+
raise Bolt::CLIError,
|
342
|
+
"A plan's 'nodes' parameter may be specified using the --nodes option, but in that " \
|
343
|
+
"case it must not be specified as a separate nodes=<value> parameter nor included " \
|
344
|
+
"in the JSON data passed in the --params option"
|
345
|
+
end
|
346
|
+
plan_arguments['nodes'] = nodes.join(',')
|
347
|
+
end
|
348
|
+
|
349
|
+
params = options[:noop] ? plan_arguments.merge('_noop' => true) : plan_arguments
|
350
|
+
plan_context = { plan_name: plan_name,
|
351
|
+
params: params }
|
352
|
+
plan_context[:description] = options[:description] if options[:description]
|
353
|
+
|
354
|
+
executor = Bolt::Executor.new(config.concurrency, @analytics, options[:noop], bundled_content: bundled_content)
|
355
|
+
executor.start_plan(plan_context)
|
356
|
+
result = pal.run_plan(plan_name, plan_arguments, executor, inventory, puppetdb_client)
|
357
|
+
|
358
|
+
# If a non-bolt exeception bubbles up the plan won't get finished
|
359
|
+
executor.finish_plan(result)
|
360
|
+
outputter.print_plan_result(result)
|
361
|
+
result.ok? ? 0 : 1
|
362
|
+
end
|
363
|
+
|
364
|
+
def install_puppetfile(puppetfile, modulepath)
|
365
|
+
if puppetfile.exist?
|
366
|
+
moduledir = modulepath.first.to_s
|
367
|
+
r10k_config = {
|
368
|
+
root: puppetfile.dirname.to_s,
|
369
|
+
puppetfile: puppetfile.to_s,
|
370
|
+
moduledir: moduledir
|
371
|
+
}
|
372
|
+
install_action = R10K::Action::Puppetfile::Install.new(r10k_config, nil)
|
373
|
+
|
374
|
+
# Override the r10k logger with a proxy to our own logger
|
375
|
+
R10K::Logging.instance_variable_set(:@outputter, Bolt::R10KLogProxy.new)
|
376
|
+
|
377
|
+
ok = install_action.call
|
378
|
+
outputter.print_puppetfile_result(ok, puppetfile, moduledir)
|
379
|
+
|
380
|
+
ok ? 0 : 1
|
381
|
+
else
|
382
|
+
raise Bolt::FileError.new("Could not find a Puppetfile at #{puppetfile}", puppetfile)
|
383
|
+
end
|
384
|
+
rescue R10K::Error => e
|
385
|
+
raise PuppetfileError, e
|
386
|
+
end
|
387
|
+
|
388
|
+
def pal
|
389
|
+
@pal ||= Bolt::PAL.new(config.modulepath, config.hiera_config, config.compile_concurrency)
|
390
|
+
end
|
391
|
+
|
333
392
|
def validate_file(type, path)
|
334
393
|
if path.nil?
|
335
394
|
raise Bolt::CLIError, "A #{type} must be specified"
|
@@ -351,18 +410,20 @@ module Bolt
|
|
351
410
|
end
|
352
411
|
|
353
412
|
def outputter
|
354
|
-
@outputter ||= Bolt::Outputter.for_format(config
|
413
|
+
@outputter ||= Bolt::Outputter.for_format(config.format, config.color, config.trace)
|
355
414
|
end
|
356
415
|
|
357
416
|
def bundled_content
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
417
|
+
if %w[plan task].include?(options[:subcommand])
|
418
|
+
default_content = Bolt::PAL.new([], nil)
|
419
|
+
plans = default_content.list_plans.each_with_object([]) do |iter, col|
|
420
|
+
col << iter&.first
|
421
|
+
end
|
422
|
+
tasks = default_content.list_tasks.each_with_object([]) do |iter, col|
|
423
|
+
col << iter&.first
|
424
|
+
end
|
425
|
+
plans.concat tasks
|
364
426
|
end
|
365
|
-
plans.concat tasks
|
366
427
|
end
|
367
428
|
end
|
368
429
|
end
|