bolt 2.26.0 → 2.31.0
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of bolt might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/Puppetfile +13 -12
- data/bolt-modules/boltlib/lib/puppet/functions/write_file.rb +2 -2
- data/lib/bolt/analytics.rb +4 -0
- data/lib/bolt/applicator.rb +19 -18
- data/lib/bolt/bolt_option_parser.rb +112 -22
- data/lib/bolt/catalog.rb +1 -1
- data/lib/bolt/cli.rb +210 -174
- data/lib/bolt/config.rb +22 -2
- data/lib/bolt/config/modulepath.rb +30 -0
- data/lib/bolt/config/options.rb +30 -0
- data/lib/bolt/config/transport/options.rb +1 -1
- data/lib/bolt/executor.rb +1 -1
- data/lib/bolt/inventory.rb +11 -10
- data/lib/bolt/logger.rb +26 -19
- data/lib/bolt/module_installer.rb +242 -0
- data/lib/bolt/outputter.rb +4 -0
- data/lib/bolt/outputter/human.rb +77 -17
- data/lib/bolt/outputter/json.rb +21 -6
- data/lib/bolt/outputter/logger.rb +2 -2
- data/lib/bolt/pal.rb +46 -25
- data/lib/bolt/plugin.rb +1 -1
- data/lib/bolt/plugin/module.rb +1 -1
- data/lib/bolt/project.rb +62 -12
- data/lib/bolt/project_migrator.rb +80 -0
- data/lib/bolt/project_migrator/base.rb +39 -0
- data/lib/bolt/project_migrator/config.rb +67 -0
- data/lib/bolt/project_migrator/inventory.rb +67 -0
- data/lib/bolt/project_migrator/modules.rb +198 -0
- data/lib/bolt/puppetfile.rb +149 -0
- data/lib/bolt/puppetfile/installer.rb +43 -0
- data/lib/bolt/puppetfile/module.rb +93 -0
- data/lib/bolt/rerun.rb +1 -1
- data/lib/bolt/result.rb +15 -0
- data/lib/bolt/shell/bash.rb +4 -3
- data/lib/bolt/transport/base.rb +4 -4
- data/lib/bolt/transport/ssh/connection.rb +1 -1
- data/lib/bolt/util.rb +51 -10
- data/lib/bolt/version.rb +1 -1
- data/lib/bolt_server/acl.rb +2 -2
- data/lib/bolt_server/base_config.rb +3 -3
- data/lib/bolt_server/config.rb +1 -1
- data/lib/bolt_server/file_cache.rb +11 -11
- data/lib/bolt_server/transport_app.rb +206 -27
- data/lib/bolt_spec/bolt_context.rb +8 -6
- data/lib/bolt_spec/plans.rb +1 -1
- data/lib/bolt_spec/plans/mock_executor.rb +1 -1
- data/lib/bolt_spec/run.rb +1 -1
- metadata +14 -6
- data/lib/bolt/project_migrate.rb +0 -138
- data/lib/bolt_server/pe/pal.rb +0 -67
@@ -0,0 +1,149 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bolt/error'
|
4
|
+
require 'bolt/puppetfile/module'
|
5
|
+
|
6
|
+
# This class manages the logical contents of a Puppetfile. It includes methods
|
7
|
+
# for parsing a Puppetfile and its modules, resolving module dependencies,
|
8
|
+
# and writing a Puppetfile.
|
9
|
+
#
|
10
|
+
module Bolt
|
11
|
+
class Puppetfile
|
12
|
+
attr_reader :modules
|
13
|
+
|
14
|
+
def initialize(modules = [])
|
15
|
+
@modules = Set.new
|
16
|
+
add_modules(modules)
|
17
|
+
end
|
18
|
+
|
19
|
+
# Loads a Puppetfile and parses its module specifications, returning a
|
20
|
+
# Bolt::Puppetfile object with the modules set.
|
21
|
+
#
|
22
|
+
def self.parse(path, skip_unsupported_modules: false)
|
23
|
+
require 'puppetfile-resolver'
|
24
|
+
require 'puppetfile-resolver/puppetfile/parser/r10k_eval'
|
25
|
+
|
26
|
+
begin
|
27
|
+
parsed = ::PuppetfileResolver::Puppetfile::Parser::R10KEval.parse(File.read(path))
|
28
|
+
rescue StandardError => e
|
29
|
+
raise Bolt::Error.new(
|
30
|
+
"Unable to parse Puppetfile #{path}: #{e.message}",
|
31
|
+
'bolt/puppetfile-parsing'
|
32
|
+
)
|
33
|
+
end
|
34
|
+
|
35
|
+
unless parsed.valid?
|
36
|
+
# valid? Just checks if validation_errors is empty, so if we get here we know it's not.
|
37
|
+
raise Bolt::ValidationError, <<~MSG
|
38
|
+
Unable to parse Puppetfile #{path}:
|
39
|
+
#{parsed.validation_errors.join("\n\n")}.
|
40
|
+
This may not be a Puppetfile managed by Bolt.
|
41
|
+
MSG
|
42
|
+
end
|
43
|
+
|
44
|
+
modules = parsed.modules.each_with_object([]) do |mod, acc|
|
45
|
+
unless mod.instance_of? PuppetfileResolver::Puppetfile::ForgeModule
|
46
|
+
next if skip_unsupported_modules
|
47
|
+
|
48
|
+
raise Bolt::ValidationError,
|
49
|
+
"Module '#{mod.title}' is not a Puppet Forge module. Unable to "\
|
50
|
+
"parse Puppetfile #{path}."
|
51
|
+
end
|
52
|
+
|
53
|
+
acc << Bolt::Puppetfile::Module.new(mod.owner, mod.name, mod.version)
|
54
|
+
end
|
55
|
+
|
56
|
+
new(modules)
|
57
|
+
end
|
58
|
+
|
59
|
+
# Writes a Puppetfile that includes specifications for each of the
|
60
|
+
# modules.
|
61
|
+
#
|
62
|
+
def write(path, moduledir = nil)
|
63
|
+
File.open(path, 'w') do |file|
|
64
|
+
if moduledir
|
65
|
+
file.puts "# This Puppetfile is managed by Bolt. Do not edit."
|
66
|
+
file.puts "# For more information, see https://pup.pt/bolt-modules"
|
67
|
+
file.puts
|
68
|
+
file.puts "# The following directive installs modules to the managed moduledir."
|
69
|
+
file.puts "moduledir '#{moduledir.basename}'"
|
70
|
+
file.puts
|
71
|
+
end
|
72
|
+
|
73
|
+
modules.each { |mod| file.puts mod.to_spec }
|
74
|
+
end
|
75
|
+
rescue SystemCallError => e
|
76
|
+
raise Bolt::FileError.new(
|
77
|
+
"#{e.message}: unable to write Puppetfile.",
|
78
|
+
path
|
79
|
+
)
|
80
|
+
end
|
81
|
+
|
82
|
+
# Resolves module dependencies using the puppetfile-resolver library. The
|
83
|
+
# resolver will return a document model including all module dependencies
|
84
|
+
# and the latest version that can be installed for each. The document model
|
85
|
+
# is parsed and turned into a Set of Bolt::Puppetfile::Module objects.
|
86
|
+
#
|
87
|
+
def resolve
|
88
|
+
require 'puppetfile-resolver'
|
89
|
+
|
90
|
+
# Build the document model from the modules.
|
91
|
+
model = PuppetfileResolver::Puppetfile::Document.new('')
|
92
|
+
|
93
|
+
@modules.each do |mod|
|
94
|
+
model.add_module(
|
95
|
+
PuppetfileResolver::Puppetfile::ForgeModule.new(mod.title).tap do |tap|
|
96
|
+
tap.version = mod.version || :latest
|
97
|
+
end
|
98
|
+
)
|
99
|
+
end
|
100
|
+
|
101
|
+
# Make sure the Puppetfile model is valid.
|
102
|
+
unless model.valid?
|
103
|
+
raise Bolt::ValidationError,
|
104
|
+
"Unable to resolve dependencies for modules: #{@modules.map(&:title).join(', ')}"
|
105
|
+
end
|
106
|
+
|
107
|
+
# Create the resolver using the Puppetfile model. nil disables Puppet
|
108
|
+
# version restrictions.
|
109
|
+
resolver = PuppetfileResolver::Resolver.new(model, nil)
|
110
|
+
|
111
|
+
# Configure and resolve the dependency graph, catching any errors
|
112
|
+
# raised by puppetfile-resolver and re-raising them as Bolt errors.
|
113
|
+
begin
|
114
|
+
result = resolver.resolve(
|
115
|
+
cache: nil,
|
116
|
+
ui: nil,
|
117
|
+
module_paths: [],
|
118
|
+
allow_missing_modules: false
|
119
|
+
)
|
120
|
+
rescue StandardError => e
|
121
|
+
raise Bolt::Error.new(e.message, 'bolt/puppetfile-resolver-error')
|
122
|
+
end
|
123
|
+
|
124
|
+
# Turn specifications into module objects. This will skip over anything that is not
|
125
|
+
# a module specification (i.e. a Puppet version specification).
|
126
|
+
@modules = result.specifications.each_with_object(Set.new) do |(_name, spec), acc|
|
127
|
+
next unless spec.instance_of? PuppetfileResolver::Models::ModuleSpecification
|
128
|
+
acc << Bolt::Puppetfile::Module.new(spec.owner, spec.name, spec.version.to_s)
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
# Adds to the set of modules.
|
133
|
+
#
|
134
|
+
def add_modules(modules)
|
135
|
+
Array(modules).each do |mod|
|
136
|
+
case mod
|
137
|
+
when Bolt::Puppetfile::Module
|
138
|
+
@modules << mod
|
139
|
+
when Hash
|
140
|
+
@modules << Bolt::Puppetfile::Module.from_hash(mod)
|
141
|
+
else
|
142
|
+
raise Bolt::ValidationError, "Module must be a Bolt::Puppetfile::Module or Hash."
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
@modules
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'r10k/cli'
|
4
|
+
require 'bolt/r10k_log_proxy'
|
5
|
+
require 'bolt/error'
|
6
|
+
|
7
|
+
# This class is used to install modules from a Puppetfile to a module directory.
|
8
|
+
#
|
9
|
+
module Bolt
|
10
|
+
class Puppetfile
|
11
|
+
class Installer
|
12
|
+
def initialize(config = {})
|
13
|
+
@config = config
|
14
|
+
end
|
15
|
+
|
16
|
+
def install(path, moduledir)
|
17
|
+
unless File.exist?(path)
|
18
|
+
raise Bolt::FileError.new(
|
19
|
+
"Could not find a Puppetfile at #{path}",
|
20
|
+
path
|
21
|
+
)
|
22
|
+
end
|
23
|
+
|
24
|
+
r10k_opts = {
|
25
|
+
root: File.dirname(path),
|
26
|
+
puppetfile: path.to_s,
|
27
|
+
moduledir: moduledir.to_s
|
28
|
+
}
|
29
|
+
|
30
|
+
settings = R10K::Settings.global_settings.evaluate(@config)
|
31
|
+
R10K::Initializers::GlobalInitializer.new(settings).call
|
32
|
+
install_action = R10K::Action::Puppetfile::Install.new(r10k_opts, nil)
|
33
|
+
|
34
|
+
# Override the r10k logger with a proxy to our own logger
|
35
|
+
R10K::Logging.instance_variable_set(:@outputter, Bolt::R10KLogProxy.new)
|
36
|
+
|
37
|
+
install_action.call
|
38
|
+
rescue R10K::Error => e
|
39
|
+
raise PuppetfileError, e
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bolt/error'
|
4
|
+
|
5
|
+
# This class represents a module specification. It used by the Bolt::Puppetfile
|
6
|
+
# class to have a consistent API for accessing a module's attributes.
|
7
|
+
#
|
8
|
+
module Bolt
|
9
|
+
class Puppetfile
|
10
|
+
class Module
|
11
|
+
attr_reader :owner, :name, :version
|
12
|
+
|
13
|
+
def initialize(owner, name, version = nil)
|
14
|
+
@owner = owner
|
15
|
+
@name = name
|
16
|
+
|
17
|
+
if version.is_a?(String)
|
18
|
+
@version = version[0] == '=' ? version[1..-1] : version
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
# Creates a new module from a hash.
|
23
|
+
#
|
24
|
+
def self.from_hash(mod)
|
25
|
+
unless mod['name'].is_a?(String)
|
26
|
+
raise Bolt::ValidationError,
|
27
|
+
"Module name must be a String, not #{mod['name'].inspect}"
|
28
|
+
end
|
29
|
+
|
30
|
+
owner, name = mod['name'].tr('/', '-').split('-', 2)
|
31
|
+
|
32
|
+
unless owner && name
|
33
|
+
raise Bolt::ValidationError, "Module name #{mod['name']} must include both the owner and module name."
|
34
|
+
end
|
35
|
+
|
36
|
+
new(owner, name, mod['version_requirement'])
|
37
|
+
end
|
38
|
+
|
39
|
+
# Returns the module's title.
|
40
|
+
#
|
41
|
+
def title
|
42
|
+
"#{@owner}-#{@name}"
|
43
|
+
end
|
44
|
+
alias to_s title
|
45
|
+
|
46
|
+
# Checks two modules for equality.
|
47
|
+
#
|
48
|
+
def eql?(other)
|
49
|
+
self.class == other.class &&
|
50
|
+
@owner == other.owner &&
|
51
|
+
@name == other.name &&
|
52
|
+
versions_intersect?(other)
|
53
|
+
end
|
54
|
+
alias == eql?
|
55
|
+
|
56
|
+
# Returns true if the versions of two modules intersect. Used to determine
|
57
|
+
# if an installed module satisfies the version requirement of another.
|
58
|
+
#
|
59
|
+
def versions_intersect?(other)
|
60
|
+
range = ::SemanticPuppet::VersionRange.parse(@version || '')
|
61
|
+
other_range = ::SemanticPuppet::VersionRange.parse(other.version || '')
|
62
|
+
|
63
|
+
range.intersection(other_range) != ::SemanticPuppet::VersionRange::EMPTY_RANGE
|
64
|
+
end
|
65
|
+
|
66
|
+
# Hashes the module.
|
67
|
+
#
|
68
|
+
def hash
|
69
|
+
[@owner, @name].hash
|
70
|
+
end
|
71
|
+
|
72
|
+
# Returns a hash representation similar to the module
|
73
|
+
# declaration.
|
74
|
+
#
|
75
|
+
def to_hash
|
76
|
+
{
|
77
|
+
'name' => title,
|
78
|
+
'version_requirement' => version
|
79
|
+
}.compact
|
80
|
+
end
|
81
|
+
|
82
|
+
# Returns the Puppetfile specification for the module.
|
83
|
+
#
|
84
|
+
def to_spec
|
85
|
+
if @version
|
86
|
+
"mod #{title.inspect}, #{@version.inspect}"
|
87
|
+
else
|
88
|
+
"mod #{title.inspect}"
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
data/lib/bolt/rerun.rb
CHANGED
data/lib/bolt/result.rb
CHANGED
@@ -66,9 +66,24 @@ module Bolt
|
|
66
66
|
'details' => { 'exit_code' => exit_code } }
|
67
67
|
end
|
68
68
|
|
69
|
+
if value.key?('_error')
|
70
|
+
unless value['_error'].is_a?(Hash) && value['_error'].key?('msg')
|
71
|
+
value['_error'] = {
|
72
|
+
'msg' => "Invalid error returned from task #{task}: #{value['_error'].inspect}. Error "\
|
73
|
+
"must be an object with a msg key.",
|
74
|
+
'kind' => 'bolt/invalid-task-error',
|
75
|
+
'details' => { 'original_error' => value['_error'] }
|
76
|
+
}
|
77
|
+
end
|
78
|
+
|
79
|
+
value['_error']['kind'] ||= 'bolt/error'
|
80
|
+
value['_error']['details'] ||= {}
|
81
|
+
end
|
82
|
+
|
69
83
|
if value.key?('_sensitive')
|
70
84
|
value['_sensitive'] = Puppet::Pops::Types::PSensitiveType::Sensitive.new(value['_sensitive'])
|
71
85
|
end
|
86
|
+
|
72
87
|
new(target, value: value, action: 'task', object: task)
|
73
88
|
end
|
74
89
|
|
data/lib/bolt/shell/bash.rb
CHANGED
@@ -143,7 +143,7 @@ module Bolt
|
|
143
143
|
|
144
144
|
execute_options[:stdin] = stdin
|
145
145
|
execute_options[:sudoable] = true if run_as
|
146
|
-
output = execute(remote_task_path, execute_options)
|
146
|
+
output = execute(remote_task_path, **execute_options)
|
147
147
|
end
|
148
148
|
Bolt::Result.for_task(target, output.stdout.string,
|
149
149
|
output.stderr.string,
|
@@ -202,13 +202,14 @@ module Bolt
|
|
202
202
|
end
|
203
203
|
|
204
204
|
def handle_sudo_errors(err)
|
205
|
-
|
205
|
+
case err
|
206
|
+
when /^#{conn.user} is not in the sudoers file\./
|
206
207
|
@logger.trace { err }
|
207
208
|
raise Bolt::Node::EscalateError.new(
|
208
209
|
"User #{conn.user} does not have sudo permission on #{target}",
|
209
210
|
'SUDO_DENIED'
|
210
211
|
)
|
211
|
-
|
212
|
+
when /^Sorry, try again\./
|
212
213
|
@logger.trace { err }
|
213
214
|
raise Bolt::Node::EscalateError.new(
|
214
215
|
"Sudo password for user #{conn.user} not recognized on #{target}",
|
data/lib/bolt/transport/base.rb
CHANGED
@@ -47,10 +47,10 @@ module Bolt
|
|
47
47
|
callback&.call(type: :node_start, target: target)
|
48
48
|
|
49
49
|
result = begin
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
50
|
+
yield
|
51
|
+
rescue StandardError, NotImplementedError => e
|
52
|
+
Bolt::Result.from_exception(target, e, action: action)
|
53
|
+
end
|
54
54
|
|
55
55
|
callback&.call(type: :node_result, result: result)
|
56
56
|
result
|
@@ -30,7 +30,7 @@ module Bolt
|
|
30
30
|
@transport_logger = transport_logger
|
31
31
|
@logger.trace("Initializing ssh connection to #{@target.safe_name}")
|
32
32
|
|
33
|
-
if target.options['private-key']
|
33
|
+
if target.options['private-key'].instance_of?(String)
|
34
34
|
begin
|
35
35
|
Bolt::Util.validate_file('ssh key', target.options['private-key'])
|
36
36
|
rescue Bolt::FileError => e
|
data/lib/bolt/util.rb
CHANGED
@@ -3,6 +3,25 @@
|
|
3
3
|
module Bolt
|
4
4
|
module Util
|
5
5
|
class << self
|
6
|
+
# Gets input for an argument.
|
7
|
+
def get_arg_input(value)
|
8
|
+
if value.start_with?('@')
|
9
|
+
file = value.sub(/^@/, '')
|
10
|
+
read_arg_file(file)
|
11
|
+
elsif value == '-'
|
12
|
+
$stdin.read
|
13
|
+
else
|
14
|
+
value
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
# Reads a file passed as an argument to a command.
|
19
|
+
def read_arg_file(file)
|
20
|
+
File.read(File.expand_path(file))
|
21
|
+
rescue StandardError => e
|
22
|
+
raise Bolt::FileError.new("Error attempting to read #{file}: #{e}", file)
|
23
|
+
end
|
24
|
+
|
6
25
|
def read_yaml_hash(path, file_name)
|
7
26
|
require 'yaml'
|
8
27
|
|
@@ -179,16 +198,16 @@ module Bolt
|
|
179
198
|
# object was frozen
|
180
199
|
frozen = obj.frozen?
|
181
200
|
cl = begin
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
201
|
+
obj.clone(freeze: false)
|
202
|
+
# Some datatypes, such as FalseClass, can't be unfrozen. These
|
203
|
+
# aren't the types we recurse on, so we can leave them frozen
|
204
|
+
rescue ArgumentError => e
|
205
|
+
if e.message =~ /can't unfreeze/
|
206
|
+
obj.clone
|
207
|
+
else
|
208
|
+
raise e
|
209
|
+
end
|
210
|
+
end
|
192
211
|
rescue *error_types
|
193
212
|
cloned[obj.object_id] = obj
|
194
213
|
obj
|
@@ -280,6 +299,28 @@ module Bolt
|
|
280
299
|
raise Bolt::ValidationError, "path must be a String, received #{path.class} #{path}" unless path.is_a?(String)
|
281
300
|
path.split(%r{[/\\]}).last
|
282
301
|
end
|
302
|
+
|
303
|
+
# Prompts yes or no, returning true for yes and false for no.
|
304
|
+
#
|
305
|
+
def prompt_yes_no(prompt, outputter)
|
306
|
+
choices = {
|
307
|
+
'y' => true,
|
308
|
+
'yes' => true,
|
309
|
+
'n' => false,
|
310
|
+
'no' => false
|
311
|
+
}
|
312
|
+
|
313
|
+
loop do
|
314
|
+
outputter.print_prompt("#{prompt} ([y]es/[n]o) ")
|
315
|
+
response = $stdin.gets.to_s.downcase.chomp
|
316
|
+
|
317
|
+
if choices.key?(response)
|
318
|
+
return choices[response]
|
319
|
+
else
|
320
|
+
outputter.print_prompt_error("Invalid response, must pick [y]es or [n]o")
|
321
|
+
end
|
322
|
+
end
|
323
|
+
end
|
283
324
|
end
|
284
325
|
end
|
285
326
|
end
|