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.

Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/Puppetfile +13 -12
  3. data/bolt-modules/boltlib/lib/puppet/functions/write_file.rb +2 -2
  4. data/lib/bolt/analytics.rb +4 -0
  5. data/lib/bolt/applicator.rb +19 -18
  6. data/lib/bolt/bolt_option_parser.rb +112 -22
  7. data/lib/bolt/catalog.rb +1 -1
  8. data/lib/bolt/cli.rb +210 -174
  9. data/lib/bolt/config.rb +22 -2
  10. data/lib/bolt/config/modulepath.rb +30 -0
  11. data/lib/bolt/config/options.rb +30 -0
  12. data/lib/bolt/config/transport/options.rb +1 -1
  13. data/lib/bolt/executor.rb +1 -1
  14. data/lib/bolt/inventory.rb +11 -10
  15. data/lib/bolt/logger.rb +26 -19
  16. data/lib/bolt/module_installer.rb +242 -0
  17. data/lib/bolt/outputter.rb +4 -0
  18. data/lib/bolt/outputter/human.rb +77 -17
  19. data/lib/bolt/outputter/json.rb +21 -6
  20. data/lib/bolt/outputter/logger.rb +2 -2
  21. data/lib/bolt/pal.rb +46 -25
  22. data/lib/bolt/plugin.rb +1 -1
  23. data/lib/bolt/plugin/module.rb +1 -1
  24. data/lib/bolt/project.rb +62 -12
  25. data/lib/bolt/project_migrator.rb +80 -0
  26. data/lib/bolt/project_migrator/base.rb +39 -0
  27. data/lib/bolt/project_migrator/config.rb +67 -0
  28. data/lib/bolt/project_migrator/inventory.rb +67 -0
  29. data/lib/bolt/project_migrator/modules.rb +198 -0
  30. data/lib/bolt/puppetfile.rb +149 -0
  31. data/lib/bolt/puppetfile/installer.rb +43 -0
  32. data/lib/bolt/puppetfile/module.rb +93 -0
  33. data/lib/bolt/rerun.rb +1 -1
  34. data/lib/bolt/result.rb +15 -0
  35. data/lib/bolt/shell/bash.rb +4 -3
  36. data/lib/bolt/transport/base.rb +4 -4
  37. data/lib/bolt/transport/ssh/connection.rb +1 -1
  38. data/lib/bolt/util.rb +51 -10
  39. data/lib/bolt/version.rb +1 -1
  40. data/lib/bolt_server/acl.rb +2 -2
  41. data/lib/bolt_server/base_config.rb +3 -3
  42. data/lib/bolt_server/config.rb +1 -1
  43. data/lib/bolt_server/file_cache.rb +11 -11
  44. data/lib/bolt_server/transport_app.rb +206 -27
  45. data/lib/bolt_spec/bolt_context.rb +8 -6
  46. data/lib/bolt_spec/plans.rb +1 -1
  47. data/lib/bolt_spec/plans/mock_executor.rb +1 -1
  48. data/lib/bolt_spec/run.rb +1 -1
  49. metadata +14 -6
  50. data/lib/bolt/project_migrate.rb +0 -138
  51. 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
@@ -53,7 +53,7 @@ module Bolt
53
53
  end
54
54
  end
55
55
  rescue StandardError => e
56
- @logger.warn("Failed to save result to #{@path}: #{e.message}")
56
+ Bolt::Logger.warn_once('unwriteable_file', "Failed to save result to #{@path}: #{e.message}")
57
57
  end
58
58
  end
59
59
  end
@@ -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
 
@@ -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
- if err =~ /^#{conn.user} is not in the sudoers file\./
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
- elsif err =~ /^Sorry, try again\./
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}",
@@ -47,10 +47,10 @@ module Bolt
47
47
  callback&.call(type: :node_start, target: target)
48
48
 
49
49
  result = begin
50
- yield
51
- rescue StandardError, NotImplementedError => e
52
- Bolt::Result.from_exception(target, e, action: action)
53
- end
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']&.instance_of?(String)
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
@@ -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
- obj.clone(freeze: false)
183
- # Some datatypes, such as FalseClass, can't be unfrozen. These
184
- # aren't the types we recurse on, so we can leave them frozen
185
- rescue ArgumentError => e
186
- if e.message =~ /can't unfreeze/
187
- obj.clone
188
- else
189
- raise e
190
- end
191
- end
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