bolt 2.33.2 → 2.38.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 (77) 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 +154 -249
  16. data/lib/bolt/config.rb +188 -55
  17. data/lib/bolt/config/options.rb +147 -87
  18. data/lib/bolt/config/transport/base.rb +10 -19
  19. data/lib/bolt/config/transport/local.rb +1 -7
  20. data/lib/bolt/config/transport/options.rb +10 -68
  21. data/lib/bolt/config/transport/ssh.rb +8 -14
  22. data/lib/bolt/error.rb +33 -3
  23. data/lib/bolt/executor.rb +92 -6
  24. data/lib/bolt/inventory.rb +25 -0
  25. data/lib/bolt/inventory/group.rb +2 -1
  26. data/lib/bolt/inventory/options.rb +130 -0
  27. data/lib/bolt/inventory/target.rb +10 -11
  28. data/lib/bolt/module_installer.rb +21 -13
  29. data/lib/bolt/module_installer/resolver.rb +1 -1
  30. data/lib/bolt/outputter.rb +19 -5
  31. data/lib/bolt/outputter/human.rb +41 -10
  32. data/lib/bolt/outputter/json.rb +1 -1
  33. data/lib/bolt/outputter/logger.rb +1 -1
  34. data/lib/bolt/outputter/rainbow.rb +13 -2
  35. data/lib/bolt/pal.rb +19 -7
  36. data/lib/bolt/pal/yaml_plan.rb +7 -0
  37. data/lib/bolt/plan_creator.rb +160 -0
  38. data/lib/bolt/plugin.rb +42 -13
  39. data/lib/bolt/plugin/cache.rb +76 -0
  40. data/lib/bolt/plugin/module.rb +4 -4
  41. data/lib/bolt/plugin/puppetdb.rb +1 -1
  42. data/lib/bolt/project.rb +59 -40
  43. data/lib/bolt/project_manager.rb +201 -0
  44. data/lib/bolt/{project_migrator/config.rb → project_manager/config_migrator.rb} +51 -5
  45. data/lib/bolt/{project_migrator/inventory.rb → project_manager/inventory_migrator.rb} +5 -5
  46. data/lib/bolt/{project_migrator/base.rb → project_manager/migrator.rb} +2 -2
  47. data/lib/bolt/{project_migrator/modules.rb → project_manager/module_migrator.rb} +5 -3
  48. data/lib/bolt/puppetdb/client.rb +11 -2
  49. data/lib/bolt/puppetdb/config.rb +9 -8
  50. data/lib/bolt/rerun.rb +1 -5
  51. data/lib/bolt/shell/bash.rb +8 -2
  52. data/lib/bolt/shell/powershell.rb +22 -4
  53. data/lib/bolt/target.rb +4 -0
  54. data/lib/bolt/task/run.rb +1 -1
  55. data/lib/bolt/transport/local.rb +13 -0
  56. data/lib/bolt/transport/orch.rb +0 -5
  57. data/lib/bolt/transport/orch/connection.rb +10 -3
  58. data/lib/bolt/transport/remote.rb +1 -1
  59. data/lib/bolt/transport/ssh/exec_connection.rb +6 -2
  60. data/lib/bolt/util.rb +41 -7
  61. data/lib/bolt/validator.rb +226 -0
  62. data/lib/bolt/version.rb +1 -1
  63. data/lib/bolt/yarn.rb +23 -0
  64. data/lib/bolt_server/base_config.rb +3 -1
  65. data/lib/bolt_server/config.rb +3 -1
  66. data/lib/bolt_server/file_cache.rb +2 -0
  67. data/lib/bolt_server/plugin.rb +13 -0
  68. data/lib/bolt_server/plugin/puppet_connect_data.rb +37 -0
  69. data/lib/bolt_server/schemas/connect-data.json +22 -0
  70. data/lib/bolt_server/schemas/partials/task.json +2 -2
  71. data/lib/bolt_server/transport_app.rb +82 -23
  72. data/lib/bolt_spec/plans/mock_executor.rb +4 -1
  73. data/libexec/apply_catalog.rb +1 -1
  74. data/libexec/custom_facts.rb +1 -1
  75. data/libexec/query_resources.rb +1 -1
  76. metadata +22 -13
  77. 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
 
@@ -61,6 +61,7 @@ module Bolt
61
61
  # Create specs to resolve from
62
62
  specs = Bolt::ModuleInstaller::Specs.new(modules.map(&:to_hash))
63
63
 
64
+ @outputter.start_spin
64
65
  # Attempt to resolve dependencies
65
66
  begin
66
67
  @outputter.print_message('')
@@ -72,6 +73,7 @@ module Bolt
72
73
  end
73
74
 
74
75
  migrate_managed_modules(puppetfile, puppetfile_path, managed_moduledir)
76
+ @outputter.stop_spin
75
77
 
76
78
  # Move remaining modules to 'modules'
77
79
  consolidate_modules(modulepath)
@@ -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
@@ -19,6 +18,7 @@ module Bolt
19
18
  def query_certnames(query)
20
19
  return [] unless query
21
20
 
21
+ @logger.debug("Querying certnames")
22
22
  results = make_query(query)
23
23
 
24
24
  if results&.first && !results.first&.key?('certname')
@@ -35,6 +35,8 @@ module Bolt
35
35
  certnames.uniq!
36
36
  name_query = certnames.map { |c| ["=", "certname", c] }
37
37
  name_query.insert(0, "or")
38
+
39
+ @logger.debug("Querying certnames")
38
40
  result = make_query(name_query, 'inventory')
39
41
 
40
42
  result&.each_with_object({}) do |node, coll|
@@ -53,6 +55,8 @@ module Bolt
53
55
  facts_query.insert(0, "or")
54
56
 
55
57
  query = ['and', name_query, facts_query]
58
+
59
+ @logger.debug("Querying certnames")
56
60
  result = make_query(query, 'fact-contents')
57
61
  result.map! { |h| h.delete_if { |k, _v| %w[environment name].include?(k) } }
58
62
  result.group_by { |c| c['certname'] }
@@ -64,11 +68,13 @@ module Bolt
64
68
  url += "/#{path}" if path
65
69
 
66
70
  begin
71
+ @logger.debug("Sending PuppetDB query to #{url}")
67
72
  response = http_client.post(url, body: body, header: headers)
68
73
  rescue StandardError => e
69
74
  raise Bolt::PuppetDBFailoverError, "Failed to query PuppetDB: #{e}"
70
75
  end
71
76
 
77
+ @logger.debug("Got response code #{response.code} from PuppetDB")
72
78
  if response.code != 200
73
79
  msg = "Failed to query PuppetDB: #{response.body}"
74
80
  if response.code == 400
@@ -93,6 +99,7 @@ module Bolt
93
99
  return @http if @http
94
100
  # lazy-load expensive gem code
95
101
  require 'httpclient'
102
+ @logger.trace("Creating HTTP Client")
96
103
  @http = HTTPClient.new
97
104
  @http.ssl_config.set_client_cert_file(@config.cert, @config.key) if @config.cert
98
105
  @http.ssl_config.add_trust_ca(@config.cacert)
@@ -108,13 +115,15 @@ module Bolt
108
115
  end
109
116
 
110
117
  def uri
118
+ require 'addressable/uri'
119
+
111
120
  @current_url ||= (@config.server_urls - @bad_urls).first
112
121
  unless @current_url
113
122
  msg = "Failed to connect to all PuppetDB server_urls: #{@config.server_urls.to_a.join(', ')}."
114
123
  raise Bolt::PuppetDBError, msg
115
124
  end
116
125
 
117
- uri = URI.parse(@current_url)
126
+ uri = Addressable::URI.parse(@current_url)
118
127
  uri.port ||= 8081
119
128
  uri
120
129
  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
@@ -177,7 +193,8 @@ module Bolt
177
193
  def run_command(command, options = {}, position = [])
178
194
  command = [*env_declarations(options[:env_vars]), command].join("\r\n") if options[:env_vars]
179
195
 
180
- output = execute(command)
196
+ wrap_command = conn.is_a?(Bolt::Transport::Local::Connection)
197
+ output = execute(command, wrap_command)
181
198
  Bolt::Result.for_command(target,
182
199
  output.stdout.string,
183
200
  output.stderr.string,
@@ -268,8 +285,9 @@ module Bolt
268
285
  end
269
286
  end
270
287
 
271
- def execute(command)
272
- if conn.max_command_length && command.length > conn.max_command_length
288
+ def execute(command, wrap_command = false)
289
+ if (conn.max_command_length && command.length > conn.max_command_length) ||
290
+ wrap_command
273
291
  return with_tmpdir do |dir|
274
292
  command += "\r\nif (!$?) { if($LASTEXITCODE) { exit $LASTEXITCODE } else { exit 1 } }"
275
293
  script_file = File.join(dir, "#{SecureRandom.uuid}_wrapper.ps1")
@@ -80,6 +80,10 @@ module Bolt
80
80
  inventory_target.resources
81
81
  end
82
82
 
83
+ def set_local_defaults
84
+ inventory_target.set_local_defaults
85
+ end
86
+
83
87
  # rubocop:disable Naming/AccessorMethodName
84
88
  def set_resource(resource)
85
89
  inventory_target.set_resource(resource)
@@ -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)
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'bolt/logger'
3
4
  require 'bolt/transport/simple'
4
5
 
5
6
  module Bolt
@@ -10,6 +11,18 @@ module Bolt
10
11
  end
11
12
 
12
13
  def with_connection(target)
14
+ if target.transport_config['bundled-ruby'] || target.name == 'localhost'
15
+ target.set_local_defaults
16
+ end
17
+
18
+ if target.name != 'localhost' &&
19
+ !target.transport_config.key?('bundled-ruby')
20
+ msg = "The local transport will default to using Bolt's Ruby interpreter and "\
21
+ "setting the 'puppet-agent' feature in Bolt 3.0. Enable or disable these "\
22
+ "defaults by setting 'bundled-ruby' in the local transport config."
23
+ Bolt::Logger.warn_once('local default config', msg)
24
+ end
25
+
13
26
  yield Connection.new(target)
14
27
  end
15
28
  end
@@ -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 }