bolt 2.33.1 → 2.37.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 (69) hide show
  1. checksums.yaml +4 -4
  2. data/Puppetfile +1 -1
  3. data/bolt-modules/boltlib/lib/puppet/datatypes/applyresult.rb +1 -0
  4. data/bolt-modules/boltlib/lib/puppet/functions/catch_errors.rb +1 -3
  5. data/bolt-modules/boltlib/lib/puppet/functions/download_file.rb +17 -6
  6. data/bolt-modules/boltlib/lib/puppet/functions/parallelize.rb +56 -0
  7. data/bolt-modules/boltlib/lib/puppet/functions/run_command.rb +24 -6
  8. data/bolt-modules/boltlib/lib/puppet/functions/run_script.rb +27 -8
  9. data/bolt-modules/boltlib/lib/puppet/functions/run_task.rb +21 -1
  10. data/bolt-modules/boltlib/lib/puppet/functions/run_task_with.rb +18 -1
  11. data/bolt-modules/boltlib/lib/puppet/functions/upload_file.rb +24 -6
  12. data/lib/bolt/analytics.rb +27 -8
  13. data/lib/bolt/apply_result.rb +3 -3
  14. data/lib/bolt/bolt_option_parser.rb +48 -16
  15. data/lib/bolt/cli.rb +95 -227
  16. data/lib/bolt/config.rb +145 -54
  17. data/lib/bolt/config/options.rb +76 -10
  18. data/lib/bolt/config/transport/base.rb +10 -19
  19. data/lib/bolt/config/transport/local.rb +0 -7
  20. data/lib/bolt/config/transport/options.rb +1 -1
  21. data/lib/bolt/config/transport/ssh.rb +8 -14
  22. data/lib/bolt/config/validator.rb +231 -0
  23. data/lib/bolt/error.rb +33 -3
  24. data/lib/bolt/executor.rb +92 -6
  25. data/lib/bolt/inventory/group.rb +2 -1
  26. data/lib/bolt/module_installer.rb +1 -1
  27. data/lib/bolt/module_installer/specs/forge_spec.rb +5 -4
  28. data/lib/bolt/module_installer/specs/git_spec.rb +4 -3
  29. data/lib/bolt/outputter/human.rb +21 -9
  30. data/lib/bolt/outputter/rainbow.rb +1 -1
  31. data/lib/bolt/pal.rb +19 -7
  32. data/lib/bolt/pal/yaml_plan.rb +7 -0
  33. data/lib/bolt/plan_creator.rb +160 -0
  34. data/lib/bolt/plugin.rb +42 -13
  35. data/lib/bolt/plugin/cache.rb +76 -0
  36. data/lib/bolt/plugin/module.rb +4 -4
  37. data/lib/bolt/project.rb +46 -40
  38. data/lib/bolt/project_manager.rb +199 -0
  39. data/lib/bolt/{project_migrator/config.rb → project_manager/config_migrator.rb} +43 -5
  40. data/lib/bolt/{project_migrator/inventory.rb → project_manager/inventory_migrator.rb} +5 -5
  41. data/lib/bolt/{project_migrator/base.rb → project_manager/migrator.rb} +2 -2
  42. data/lib/bolt/{project_migrator/modules.rb → project_manager/module_migrator.rb} +3 -3
  43. data/lib/bolt/puppetdb/client.rb +3 -2
  44. data/lib/bolt/puppetdb/config.rb +9 -8
  45. data/lib/bolt/rerun.rb +1 -5
  46. data/lib/bolt/shell/bash.rb +8 -2
  47. data/lib/bolt/shell/powershell.rb +17 -1
  48. data/lib/bolt/task/run.rb +1 -1
  49. data/lib/bolt/transport/orch.rb +0 -5
  50. data/lib/bolt/transport/orch/connection.rb +10 -3
  51. data/lib/bolt/transport/remote.rb +1 -1
  52. data/lib/bolt/transport/ssh/exec_connection.rb +6 -2
  53. data/lib/bolt/util.rb +41 -7
  54. data/lib/bolt/version.rb +1 -1
  55. data/lib/bolt/yarn.rb +23 -0
  56. data/lib/bolt_server/base_config.rb +3 -1
  57. data/lib/bolt_server/config.rb +3 -1
  58. data/lib/bolt_server/file_cache.rb +2 -0
  59. data/lib/bolt_server/plugin.rb +13 -0
  60. data/lib/bolt_server/plugin/puppet_connect_data.rb +37 -0
  61. data/lib/bolt_server/schemas/connect-data.json +22 -0
  62. data/lib/bolt_server/schemas/partials/task.json +2 -2
  63. data/lib/bolt_server/transport_app.rb +72 -13
  64. data/lib/bolt_spec/plans/mock_executor.rb +4 -1
  65. data/libexec/apply_catalog.rb +1 -1
  66. data/libexec/custom_facts.rb +1 -1
  67. data/libexec/query_resources.rb +1 -1
  68. metadata +15 -13
  69. data/lib/bolt/project_migrator.rb +0 -80
@@ -1,17 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'bolt/project_migrator/base'
3
+ require 'bolt/project_manager/migrator'
4
4
 
5
5
  module Bolt
6
- class ProjectMigrator
7
- class Inventory < Base
6
+ class ProjectManager
7
+ class InventoryMigrator < Migrator
8
8
  def migrate(inventory_file, backup_dir)
9
- inventory_1_to_2(inventory_file, backup_dir)
9
+ inventory1to2(inventory_file, backup_dir)
10
10
  end
11
11
 
12
12
  # Migrates an inventory v1 file to inventory v2.
13
13
  #
14
- private def inventory_1_to_2(inventory_file, backup_dir)
14
+ private def inventory1to2(inventory_file, backup_dir)
15
15
  unless File.exist?(inventory_file)
16
16
  return true
17
17
  end
@@ -4,8 +4,8 @@ require 'fileutils'
4
4
  require 'bolt/error'
5
5
 
6
6
  module Bolt
7
- class ProjectMigrator
8
- class Base
7
+ class ProjectManager
8
+ class Migrator
9
9
  def initialize(outputter)
10
10
  @outputter = outputter
11
11
  end
@@ -1,10 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'bolt/project_migrator/base'
3
+ require 'bolt/project_manager/migrator'
4
4
 
5
5
  module Bolt
6
- class ProjectMigrator
7
- class Modules < Base
6
+ class ProjectManager
7
+ class ModuleMigrator < Migrator
8
8
  def migrate(project, configured_modulepath)
9
9
  return true unless project.modules.nil?
10
10
 
@@ -2,7 +2,6 @@
2
2
 
3
3
  require 'json'
4
4
  require 'logging'
5
- require 'uri'
6
5
 
7
6
  module Bolt
8
7
  module PuppetDB
@@ -108,13 +107,15 @@ module Bolt
108
107
  end
109
108
 
110
109
  def uri
110
+ require 'addressable/uri'
111
+
111
112
  @current_url ||= (@config.server_urls - @bad_urls).first
112
113
  unless @current_url
113
114
  msg = "Failed to connect to all PuppetDB server_urls: #{@config.server_urls.to_a.join(', ')}."
114
115
  raise Bolt::PuppetDBError, msg
115
116
  end
116
117
 
117
- uri = URI.parse(@current_url)
118
+ uri = Addressable::URI.parse(@current_url)
118
119
  uri.port ||= 8081
119
120
  uri
120
121
  end
@@ -6,20 +6,19 @@ require 'bolt/util'
6
6
  module Bolt
7
7
  module PuppetDB
8
8
  class Config
9
- if !ENV['HOME'].nil?
10
- DEFAULT_TOKEN = File.expand_path('~/.puppetlabs/token')
11
- DEFAULT_CONFIG = { user: File.expand_path('~/.puppetlabs/client-tools/puppetdb.conf'),
12
- global: '/etc/puppetlabs/client-tools/puppetdb.conf' }.freeze
13
- else
9
+ if ENV['HOME'].nil?
14
10
  DEFAULT_TOKEN = Bolt::Util.windows? ? 'nul' : '/dev/null'
15
11
  DEFAULT_CONFIG = { user: '/etc/puppetlabs/puppet/puppetdb.conf',
16
12
  global: '/etc/puppetlabs/puppet/puppetdb.conf' }.freeze
13
+ else
14
+ DEFAULT_TOKEN = File.expand_path('~/.puppetlabs/token')
15
+ DEFAULT_CONFIG = { user: File.expand_path('~/.puppetlabs/client-tools/puppetdb.conf'),
16
+ global: '/etc/puppetlabs/client-tools/puppetdb.conf' }.freeze
17
17
 
18
18
  end
19
19
 
20
20
  def self.default_windows_config
21
- require 'win32/dir'
22
- File.expand_path(File.join(Dir::COMMON_APPDATA, 'PuppetLabs/client-tools/puppetdb.conf'))
21
+ File.expand_path(File.join(ENV['ALLUSERSPROFILE'], 'PuppetLabs/client-tools/puppetdb.conf'))
23
22
  end
24
23
 
25
24
  def self.load_config(options, project_path = nil)
@@ -89,6 +88,8 @@ module Bolt
89
88
 
90
89
  def uri
91
90
  return @uri if @uri
91
+ require 'addressable/uri'
92
+
92
93
  uri = case @settings['server_urls']
93
94
  when String
94
95
  @settings['server_urls']
@@ -100,7 +101,7 @@ module Bolt
100
101
  raise Bolt::PuppetDBError, "server_urls must be a string or array"
101
102
  end
102
103
 
103
- @uri = URI.parse(uri)
104
+ @uri = Addressable::URI.parse(uri)
104
105
  @uri.port ||= 8081
105
106
  @uri
106
107
  end
@@ -12,15 +12,11 @@ module Bolt
12
12
  end
13
13
 
14
14
  def data
15
- @data ||= JSON.parse(File.read(@path))
15
+ @data ||= Bolt::Util.read_json_file(@path, 'rerun')
16
16
  unless @data.is_a?(Array) && @data.all? { |r| r['target'] && r['status'] }
17
17
  raise Bolt::FileError.new("Missing data in rerun file: #{@path}", @path)
18
18
  end
19
19
  @data
20
- rescue JSON::ParserError
21
- raise Bolt::FileError.new("Could not parse rerun file: #{@path}", @path)
22
- rescue IOError, SystemCallError
23
- raise Bolt::FileError.new("Could not read rerun file: #{@path}", @path)
24
20
  end
25
21
 
26
22
  def get_targets(filter)
@@ -199,7 +199,7 @@ module Bolt
199
199
  lines = buffer.split(/(?<=\n)/)
200
200
  # handle_sudo will return the line if it is not a sudo prompt or error
201
201
  lines.map! { |line| handle_sudo(inp, line, stdin) }
202
- lines.join("")
202
+ lines.join
203
203
  # If stream has reached EOF, no password prompt is expected
204
204
  # return an empty string
205
205
  rescue EOFError
@@ -436,8 +436,14 @@ module Bolt
436
436
  result_output.stderr << read_streams[err]
437
437
  result_output.exit_code = t.value.respond_to?(:exitstatus) ? t.value.exitstatus : t.value
438
438
 
439
- if result_output.exit_code == 0
439
+ case result_output.exit_code
440
+ when 0
440
441
  @logger.trace { "Command `#{command_str}` returned successfully" }
442
+ when 126
443
+ msg = "\n\nThis may be caused by the default tmpdir being mounted "\
444
+ "using 'noexec'. See http://pup.pt/task-failure for details and workarounds."
445
+ result_output.stderr << msg
446
+ @logger.trace { "Command #{command_str} failed with exit code #{result_output.exit_code}" }
441
447
  else
442
448
  @logger.trace { "Command #{command_str} failed with exit code #{result_output.exit_code}" }
443
449
  end
@@ -11,9 +11,25 @@ module Bolt
11
11
  def initialize(target, conn)
12
12
  super
13
13
 
14
- extensions = [target.options['extensions'] || []].flatten.map { |ext| ext[0] != '.' ? '.' + ext : ext }
14
+ extensions = [target.options['extensions'] || []].flatten.map { |ext| ext[0] == '.' ? ext : '.' + ext }
15
15
  extensions += target.options['interpreters'].keys if target.options['interpreters']
16
16
  @extensions = DEFAULT_EXTENSIONS + extensions
17
+ validate_ps_version
18
+ end
19
+
20
+ def validate_ps_version
21
+ version = execute("$PSVersionTable.PSVersion.Major").stdout.string.chomp
22
+ if !version.empty? && version.to_i < 3
23
+ # This lets us know how many targets have Powershell 2, and lets the
24
+ # user know how many targets they have with PS2
25
+ msg = "Detected PowerShell 2 on one or more targets.\nPowerShell 2 "\
26
+ "is deprecated, and support will be removed in Bolt 3.0. See "\
27
+ "bolt-debug.log or run with '--log-level debug' to see the full "\
28
+ "list of targets with PowerShell 2."
29
+
30
+ Bolt::Logger.deprecation_warning("PowerShell 2", msg)
31
+ @logger.debug("Detected PowerShell 2 on #{target}.")
32
+ end
17
33
  end
18
34
 
19
35
  def provided_features
@@ -42,7 +42,7 @@ module Bolt
42
42
  if targets.empty?
43
43
  Bolt::ResultSet.new([])
44
44
  else
45
- result = executor.run_task(targets, task, params, options)
45
+ result = executor.run_task(targets, task, params, options, [], :trace)
46
46
 
47
47
  if !result.ok && !options[:catch_errors]
48
48
  raise Bolt::RunFailure.new(result, 'run_task', task.name)
@@ -10,11 +10,6 @@ require 'bolt/transport/orch/connection'
10
10
  module Bolt
11
11
  module Transport
12
12
  class Orch < Base
13
- CONF_FILE = if !ENV['HOME'].nil?
14
- File.expand_path('~/.puppetlabs/client-tools/orchestrator.conf')
15
- else
16
- '/etc/puppetlabs/client-tools/orchestrator.conf'
17
- end
18
13
  BOLT_COMMAND_TASK = Struct.new(:name).new('bolt_shim::command').freeze
19
14
  BOLT_SCRIPT_TASK = Struct.new(:name).new('bolt_shim::script').freeze
20
15
  BOLT_UPLOAD_TASK = Struct.new(:name).new('bolt_shim::upload').freeze
@@ -17,13 +17,20 @@ module Bolt
17
17
  end
18
18
 
19
19
  def initialize(opts, plan_context, logger)
20
+ require 'addressable/uri'
21
+
20
22
  @logger = logger
21
23
  @key = self.class.get_key(opts)
22
- client_keys = %w[service-url token-file cacert job-poll-interval job-poll-timeout]
23
- client_opts = client_keys.each_with_object({}) do |k, acc|
24
- acc[k] = opts[k] if opts.include?(k)
24
+ client_opts = opts.slice('token-file', 'cacert', 'job-poll-interval', 'job-poll-timeout')
25
+
26
+ if opts['service-url']
27
+ uri = Addressable::URI.parse(opts['service-url'])
28
+ uri&.port ||= 8143
29
+ client_opts['service-url'] = uri.to_s
25
30
  end
31
+
26
32
  client_opts['User-Agent'] = "Bolt/#{VERSION}"
33
+
27
34
  %w[token-file cacert].each do |f|
28
35
  client_opts[f] = File.expand_path(client_opts[f]) if client_opts[f]
29
36
  end
@@ -29,7 +29,7 @@ module Bolt
29
29
  def run_task(target, task, arguments, options = {}, position = [])
30
30
  proxy_target = get_proxy(target)
31
31
  transport = @executor.transport(proxy_target.transport)
32
- arguments = arguments.merge('_target' => target.to_h.reject { |_, v| v.nil? })
32
+ arguments = arguments.merge('_target' => target.to_h.compact)
33
33
 
34
34
  remote_task = task.remote_instance
35
35
 
@@ -12,8 +12,12 @@ module Bolt
12
12
  raise Bolt::ValidationError, "Target #{target.safe_name} does not have a host" unless target.host
13
13
 
14
14
  @target = target
15
- ssh_config = Net::SSH::Config.for(target.host)
16
- @user = @target.user || ssh_config[:user] || Etc.getlogin
15
+ begin
16
+ ssh_config = Net::SSH::Config.for(target.host)
17
+ @user = @target.user || ssh_config[:user] || Etc.getlogin
18
+ rescue StandardError
19
+ @user = @target.user || Etc.getlogin
20
+ end
17
21
  @logger = Bolt::Logger.logger(self)
18
22
  end
19
23
 
@@ -22,6 +22,28 @@ module Bolt
22
22
  raise Bolt::FileError.new("Error attempting to read #{file}: #{e}", file)
23
23
  end
24
24
 
25
+ def read_json_file(path, filename)
26
+ require 'json'
27
+
28
+ logger = Bolt::Logger.logger(self)
29
+ path = File.expand_path(path)
30
+ content = JSON.parse(File.read(path))
31
+ logger.trace("Loaded #{filename} from #{path}")
32
+ content
33
+ rescue Errno::ENOENT
34
+ raise Bolt::FileError.new("Could not read #{filename} file at #{path}", path)
35
+ rescue JSON::ParserError => e
36
+ msg = "Unable to parse #{filename} file at #{path} as JSON: #{e.message}"
37
+ raise Bolt::FileError.new(msg, path)
38
+ rescue IOError, SystemCallError => e
39
+ raise Bolt::FileError.new("Could not read #{filename} file at #{path}\n#{e.message}",
40
+ path)
41
+ end
42
+
43
+ def read_optional_json_file(path, file_name)
44
+ File.exist?(path) && !File.zero?(path) ? read_yaml_hash(path, file_name) : {}
45
+ end
46
+
25
47
  def read_yaml_hash(path, file_name)
26
48
  require 'yaml'
27
49
 
@@ -29,19 +51,26 @@ module Bolt
29
51
  path = File.expand_path(path)
30
52
  content = File.open(path, "r:UTF-8") { |f| YAML.safe_load(f.read) } || {}
31
53
  unless content.is_a?(Hash)
32
- msg = "Invalid content for #{file_name} file: #{path} should be a Hash or empty, not #{content.class}"
33
- raise Bolt::FileError.new(msg, path)
54
+ raise Bolt::FileError.new(
55
+ "Invalid content for #{file_name} file at #{path}\nContent should be a Hash or empty, "\
56
+ "not #{content.class}",
57
+ path
58
+ )
34
59
  end
35
60
  logger.trace("Loaded #{file_name} from #{path}")
36
61
  content
37
62
  rescue Errno::ENOENT
38
- raise Bolt::FileError.new("Could not read #{file_name} file: #{path}", path)
63
+ raise Bolt::FileError.new("Could not read #{file_name} file at #{path}", path)
64
+ rescue Psych::SyntaxError => e
65
+ raise Bolt::FileError.new("Could not parse #{file_name} file at #{path}, line #{e.line}, "\
66
+ "column #{e.column}\n#{e.problem}",
67
+ path)
39
68
  rescue Psych::Exception => e
40
- raise Bolt::FileError.new("Could not parse #{file_name} file: #{path}\n"\
41
- "Error at line #{e.line} column #{e.column}", path)
69
+ raise Bolt::FileError.new("Could not parse #{file_name} file at #{path}\n#{e.message}",
70
+ path)
42
71
  rescue IOError, SystemCallError => e
43
- raise Bolt::FileError.new("Could not read #{file_name} file: #{path}\n"\
44
- "error: #{e}", path)
72
+ raise Bolt::FileError.new("Could not read #{file_name} file at #{path}\n#{e.message}",
73
+ path)
45
74
  end
46
75
 
47
76
  def read_optional_yaml_hash(path, file_name)
@@ -273,6 +302,11 @@ module Bolt
273
302
  !!File::ALT_SEPARATOR
274
303
  end
275
304
 
305
+ # Returns true if running in PowerShell.
306
+ def powershell?
307
+ !!ENV['PSModulePath']
308
+ end
309
+
276
310
  # Accept hash and return hash with top level keys of type "String" converted to symbols.
277
311
  def symbolize_top_level_keys(hsh)
278
312
  hsh.each_with_object({}) { |(k, v), h| k.is_a?(String) ? h[k.to_sym] = v : h[k] = v }
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Bolt
4
- VERSION = '2.33.1'
4
+ VERSION = '2.37.0'
5
5
  end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fiber'
4
+
5
+ module Bolt
6
+ class Yarn
7
+ attr_reader :fiber, :value, :index
8
+
9
+ def initialize(fiber, index)
10
+ @fiber = fiber
11
+ @index = index
12
+ @value = nil
13
+ end
14
+
15
+ def alive?
16
+ fiber.alive?
17
+ end
18
+
19
+ def resume
20
+ @value = fiber.resume
21
+ end
22
+ end
23
+ end
@@ -7,7 +7,9 @@ module BoltServer
7
7
  class BaseConfig
8
8
  def config_keys
9
9
  %w[host port ssl-cert ssl-key ssl-ca-cert
10
- ssl-cipher-suites loglevel logfile allowlist projects-dir]
10
+ ssl-cipher-suites loglevel logfile allowlist
11
+ projects-dir environments-codedir
12
+ environmentpath basemodulepath]
11
13
  end
12
14
 
13
15
  def env_keys
@@ -7,7 +7,9 @@ require 'bolt/error'
7
7
  module BoltServer
8
8
  class Config < BoltServer::BaseConfig
9
9
  def config_keys
10
- super + %w[concurrency cache-dir file-server-conn-timeout file-server-uri projects-dir]
10
+ super + %w[concurrency cache-dir file-server-conn-timeout
11
+ file-server-uri projects-dir environments-codedir
12
+ environmentpath basemodulepath]
11
13
  end
12
14
 
13
15
  def env_keys
@@ -63,6 +63,7 @@ module BoltServer
63
63
  end
64
64
 
65
65
  def client
66
+ # rubocop:disable Naming/VariableNumber
66
67
  @client ||= begin
67
68
  uri = URI(@config['file-server-uri'])
68
69
  https = Net::HTTP.new(uri.host, uri.port)
@@ -75,6 +76,7 @@ module BoltServer
75
76
  https.open_timeout = @config['file-server-conn-timeout']
76
77
  https
77
78
  end
79
+ # rubocop:enable Naming/VariableNumber
78
80
  end
79
81
 
80
82
  def request_file(path, params, file)
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bolt/error'
4
+
5
+ module BoltServer
6
+ class Plugin
7
+ class PluginNotSupported < Bolt::Error
8
+ def initialize(msg, plugin_name)
9
+ super(msg, 'bolt/plugin-not-supported', { "plugin_name" => plugin_name })
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BoltServer
4
+ class Plugin
5
+ class PuppetConnectData
6
+ def initialize(data, **_opts)
7
+ @data = data
8
+ end
9
+
10
+ def name
11
+ 'puppet_connect_data'
12
+ end
13
+
14
+ def hooks
15
+ %i[resolve_reference validate_resolve_reference]
16
+ end
17
+
18
+ def resolve_reference(opts)
19
+ key = opts['key']
20
+
21
+ @data.dig(key, 'value')
22
+ end
23
+
24
+ def validate_resolve_reference(opts)
25
+ unless opts['key']
26
+ raise Bolt::ValidationError,
27
+ "puppet_connect_data plugin requires that 'key' be specified"
28
+ end
29
+
30
+ unless @data.key?(opts['key'])
31
+ raise Bolt::ValidationError,
32
+ "puppet_connect_data plugin tried to lookup key '#{opts['key']}' but no value was found"
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end