bolt 2.9.0 → 2.13.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 +2 -2
  3. data/bolt-modules/boltlib/lib/puppet/datatypes/applyresult.rb +2 -0
  4. data/bolt-modules/boltlib/lib/puppet/datatypes/resourceinstance.rb +28 -0
  5. data/bolt-modules/boltlib/lib/puppet/datatypes/result.rb +2 -0
  6. data/bolt-modules/boltlib/lib/puppet/datatypes/resultset.rb +2 -0
  7. data/bolt-modules/boltlib/lib/puppet/datatypes/target.rb +4 -3
  8. data/bolt-modules/boltlib/lib/puppet/functions/apply_prep.rb +1 -1
  9. data/bolt-modules/boltlib/lib/puppet/functions/get_resources.rb +1 -1
  10. data/bolt-modules/boltlib/lib/puppet/functions/run_plan.rb +61 -0
  11. data/bolt-modules/boltlib/lib/puppet/functions/run_task.rb +4 -2
  12. data/bolt-modules/boltlib/lib/puppet/functions/run_task_with.rb +8 -2
  13. data/bolt-modules/boltlib/lib/puppet/functions/set_resources.rb +144 -0
  14. data/bolt-modules/boltlib/types/planresult.pp +1 -0
  15. data/bolt-modules/file/lib/puppet/functions/file/exists.rb +3 -1
  16. data/bolt-modules/file/lib/puppet/functions/file/join.rb +1 -1
  17. data/bolt-modules/file/lib/puppet/functions/file/read.rb +2 -1
  18. data/bolt-modules/file/lib/puppet/functions/file/readable.rb +3 -1
  19. data/bolt-modules/file/lib/puppet/functions/file/write.rb +3 -1
  20. data/lib/bolt/analytics.rb +21 -2
  21. data/lib/bolt/applicator.rb +7 -2
  22. data/lib/bolt/apply_result.rb +1 -1
  23. data/lib/bolt/apply_target.rb +3 -2
  24. data/lib/bolt/bolt_option_parser.rb +18 -8
  25. data/lib/bolt/catalog.rb +4 -1
  26. data/lib/bolt/cli.rb +15 -5
  27. data/lib/bolt/config.rb +44 -16
  28. data/lib/bolt/config/transport/ssh.rb +47 -1
  29. data/lib/bolt/inventory.rb +2 -1
  30. data/lib/bolt/inventory/inventory.rb +5 -0
  31. data/lib/bolt/inventory/target.rb +17 -1
  32. data/lib/bolt/node/output.rb +1 -1
  33. data/lib/bolt/pal.rb +3 -0
  34. data/lib/bolt/pal/yaml_plan.rb +1 -0
  35. data/lib/bolt/plugin.rb +13 -7
  36. data/lib/bolt/plugin/puppetdb.rb +5 -2
  37. data/lib/bolt/project.rb +25 -7
  38. data/lib/bolt/puppetdb/config.rb +14 -26
  39. data/lib/bolt/resource_instance.rb +129 -0
  40. data/lib/bolt/shell/bash.rb +1 -1
  41. data/lib/bolt/target.rb +12 -1
  42. data/lib/bolt/transport/ssh.rb +6 -2
  43. data/lib/bolt/transport/ssh/connection.rb +4 -0
  44. data/lib/bolt/transport/ssh/exec_connection.rb +110 -0
  45. data/lib/bolt/transport/winrm/connection.rb +4 -0
  46. data/lib/bolt/version.rb +1 -1
  47. data/lib/bolt_server/pe/pal.rb +1 -38
  48. data/lib/bolt_spec/bolt_context.rb +1 -4
  49. data/lib/bolt_spec/plans/mock_executor.rb +1 -0
  50. data/lib/bolt_spec/run.rb +2 -5
  51. metadata +6 -2
@@ -29,7 +29,7 @@ module Bolt
29
29
  def self.build_client
30
30
  logger = Logging.logger[self]
31
31
  begin
32
- config_file = File.expand_path('~/.puppetlabs/bolt/analytics.yaml')
32
+ config_file = config_path(logger)
33
33
  config = load_config(config_file, logger)
34
34
  rescue ArgumentError
35
35
  config = { 'disabled' => true }
@@ -51,6 +51,25 @@ module Bolt
51
51
  NoopClient.new
52
52
  end
53
53
 
54
+ def self.config_path(logger)
55
+ path = File.expand_path(File.join('~', '.puppetlabs', 'etc', 'bolt', 'analytics.yaml'))
56
+ old_path = File.expand_path(File.join('~', '.puppetlabs', 'bolt', 'analytics.yaml'))
57
+
58
+ if File.exist?(path)
59
+ if File.exist?(old_path)
60
+ message = "Detected analytics configuration files at '#{old_path}' and '#{path}'. Loading "\
61
+ "analytics configuration from '#{path}'."
62
+ logger.warn(message)
63
+ end
64
+
65
+ path
66
+ elsif File.exist?(old_path)
67
+ old_path
68
+ else
69
+ path
70
+ end
71
+ end
72
+
54
73
  def self.load_config(filename, logger)
55
74
  if File.exist?(filename)
56
75
  YAML.load_file(filename)
@@ -59,7 +78,7 @@ module Bolt
59
78
  logger.warn <<~ANALYTICS
60
79
  Bolt collects data about how you use it. You can opt out of providing this data.
61
80
 
62
- To disable analytics data collection, add this line to ~/.puppetlabs/bolt/analytics.yaml :
81
+ To disable analytics data collection, add this line to ~/.puppetlabs/etc/bolt/analytics.yaml :
63
82
  disabled: true
64
83
 
65
84
  Read more about what data Bolt collects and why here:
@@ -14,7 +14,8 @@ require 'open3'
14
14
 
15
15
  module Bolt
16
16
  class Applicator
17
- def initialize(inventory, executor, modulepath, plugin_dirs, pdb_client, hiera_config, max_compiles, apply_settings)
17
+ def initialize(inventory, executor, modulepath, plugin_dirs, project,
18
+ pdb_client, hiera_config, max_compiles, apply_settings)
18
19
  # lazy-load expensive gem code
19
20
  require 'concurrent'
20
21
 
@@ -22,6 +23,7 @@ module Bolt
22
23
  @executor = executor
23
24
  @modulepath = modulepath
24
25
  @plugin_dirs = plugin_dirs
26
+ @project = project
25
27
  @pdb_client = pdb_client
26
28
  @hiera_config = hiera_config ? validate_hiera_config(hiera_config) : nil
27
29
  @apply_settings = apply_settings || {}
@@ -34,6 +36,8 @@ module Bolt
34
36
  search_dirs << mod.plugins if mod.plugins?
35
37
  search_dirs << mod.pluginfacts if mod.pluginfacts?
36
38
  search_dirs << mod.files if mod.files?
39
+ type_files = "#{mod.path}/types"
40
+ search_dirs << type_files if File.exist?(type_files)
37
41
  search_dirs
38
42
  end
39
43
  end
@@ -181,11 +185,12 @@ module Bolt
181
185
  rich_data: true,
182
186
  symbol_as_string: true,
183
187
  type_by_reference: true,
184
- local_reference: false)
188
+ local_reference: true)
185
189
 
186
190
  scope = {
187
191
  code_ast: ast,
188
192
  modulepath: @modulepath,
193
+ project: @project.to_h,
189
194
  pdb_config: @pdb_client.config.to_hash,
190
195
  hiera_config: @hiera_config,
191
196
  plan_vars: plan_vars,
@@ -57,7 +57,7 @@ module Bolt
57
57
  msg = "Report result contains an '_output' key. Catalog application may have printed extraneous output to stdout: #{result['_output']}"
58
58
  # rubocop:enable Layout/LineLength
59
59
  else
60
- msg = "Report did not contain all expected keys missing: #{missing_keys.join(' ,')}"
60
+ msg = "Report did not contain all expected keys missing: #{missing_keys.join(', ')}"
61
61
  end
62
62
 
63
63
  { 'msg' => msg,
@@ -3,7 +3,7 @@
3
3
  module Bolt
4
4
  class ApplyTarget
5
5
  ATTRIBUTES = %i[uri name target_alias config vars facts features
6
- plugin_hooks safe_name].freeze
6
+ plugin_hooks resources safe_name].freeze
7
7
  COMPUTED = %i[host password port protocol user].freeze
8
8
 
9
9
  attr_reader(*ATTRIBUTES)
@@ -24,7 +24,8 @@ module Bolt
24
24
  facts = nil,
25
25
  vars = nil,
26
26
  features = nil,
27
- plugin_hooks = nil)
27
+ plugin_hooks = nil,
28
+ resources = nil)
28
29
  raise Bolt::Error.new("Target objects cannot be instantiated inside apply blocks", 'bolt/apply-error')
29
30
  end
30
31
  # rubocop:enable Lint/UnusedMethodArgument
@@ -11,7 +11,7 @@ module Bolt
11
11
  escalation: %w[run-as sudo-password sudo-password-prompt sudo-executable],
12
12
  run_context: %w[concurrency inventoryfile save-rerun cleanup],
13
13
  global_config_setters: %w[modulepath boltdir configfile],
14
- transports: %w[transport connect-timeout tty],
14
+ transports: %w[transport connect-timeout tty ssh-command copy-command],
15
15
  display: %w[format color verbose trace],
16
16
  global: %w[help version debug] }.freeze
17
17
 
@@ -20,7 +20,7 @@ module Bolt
20
20
  def get_help_text(subcommand, action = nil)
21
21
  case subcommand
22
22
  when 'apply'
23
- { flags: ACTION_OPTS + %w[noop execute compile-concurrency],
23
+ { flags: ACTION_OPTS + %w[noop execute compile-concurrency hiera-config],
24
24
  banner: APPLY_HELP }
25
25
  when 'command'
26
26
  case action
@@ -172,13 +172,14 @@ module Bolt
172
172
  apply
173
173
 
174
174
  USAGE
175
- bolt apply <manifest.pp> [options]
175
+ bolt apply [manifest.pp] [options]
176
176
 
177
177
  DESCRIPTION
178
178
  Apply Puppet manifest code on the specified targets.
179
179
 
180
180
  EXAMPLES
181
- bolt apply manifest.pp --targets target1,target2
181
+ bolt apply manifest.pp -t target
182
+ bolt apply -e "file { '/etc/puppetlabs': ensure => present }" -t target
182
183
  HELP
183
184
 
184
185
  COMMAND_HELP = <<~HELP
@@ -594,6 +595,7 @@ module Bolt
594
595
  HELP
595
596
 
596
597
  attr_reader :warnings
598
+
597
599
  def initialize(options)
598
600
  super()
599
601
 
@@ -655,8 +657,8 @@ module Bolt
655
657
  @options[:password] = STDIN.noecho(&:gets).chomp
656
658
  STDERR.puts
657
659
  end
658
- define('--private-key KEY', 'Private ssh key to authenticate with') do |key|
659
- @options[:'private-key'] = key
660
+ define('--private-key KEY', 'Path to private ssh key to authenticate with') do |key|
661
+ @options[:'private-key'] = File.expand_path(key)
660
662
  end
661
663
  define('--[no-]host-key-check', 'Check host keys with SSH') do |host_key_check|
662
664
  @options[:'host-key-check'] = host_key_check
@@ -688,7 +690,7 @@ module Bolt
688
690
 
689
691
  separator "\nRUN CONTEXT OPTIONS"
690
692
  define('-c', '--concurrency CONCURRENCY', Integer,
691
- 'Maximum number of simultaneous connections (default: 100)') do |concurrency|
693
+ 'Maximum number of simultaneous connections') do |concurrency|
692
694
  @options[:concurrency] = concurrency
693
695
  end
694
696
  define('--compile-concurrency CONCURRENCY', Integer,
@@ -718,7 +720,7 @@ module Bolt
718
720
  end
719
721
  define('--hiera-config FILEPATH',
720
722
  'Specify where to load Hiera config from (default: ~/.puppetlabs/bolt/hiera.yaml)') do |path|
721
- @options[:'hiera-config'] = path
723
+ @options[:'hiera-config'] = File.expand_path(path)
722
724
  end
723
725
  define('-i', '--inventoryfile FILEPATH',
724
726
  'Specify where to load inventory from (default: ~/.puppetlabs/bolt/inventory.yaml)') do |path|
@@ -741,6 +743,14 @@ module Bolt
741
743
  "Specify a default transport: #{TRANSPORTS.keys.join(', ')}") do |t|
742
744
  @options[:transport] = t
743
745
  end
746
+ define('--ssh-command EXEC', "Executable to use instead of the net-ssh ruby library. ",
747
+ "This option is experimental.") do |exec|
748
+ @options[:'ssh-command'] = exec
749
+ end
750
+ define('--copy-command EXEC', "Command to copy files to remote hosts if using external SSH. ",
751
+ "This option is experimental.") do |exec|
752
+ @options[:'copy-command'] = exec
753
+ end
744
754
  define('--connect-timeout TIMEOUT', Integer, 'Connection timeout (defaults vary)') do |timeout|
745
755
  @options[:'connect-timeout'] = timeout
746
756
  end
@@ -58,6 +58,8 @@ module Bolt
58
58
  target = request['target']
59
59
  pdb_client = Bolt::PuppetDB::Client.new(Bolt::PuppetDB::Config.new(request['pdb_config']))
60
60
  options = request['puppet_config'] || {}
61
+ project = request['project'] || {}
62
+ bolt_project = Struct.new(:name, :path).new(project['name'], project['path']) unless project.empty?
61
63
  with_puppet_settings(request['hiera_config']) do
62
64
  Puppet[:rich_data] = true
63
65
  Puppet[:node_name_value] = target['name']
@@ -67,7 +69,8 @@ module Bolt
67
69
  Puppet::Pal.in_tmp_environment('bolt_catalog', env_conf) do |pal|
68
70
  inv = Bolt::ApplyInventory.new(request['config'])
69
71
  Puppet.override(bolt_pdb_client: pdb_client,
70
- bolt_inventory: inv) do
72
+ bolt_inventory: inv,
73
+ bolt_project: bolt_project) do
71
74
  Puppet.lookup(:pal_current_node).trusted_data = target['trusted']
72
75
  pal.with_catalog_compiler do |compiler|
73
76
  # Deserializing needs to happen inside the catalog compiler so
@@ -124,8 +124,9 @@ module Bolt
124
124
 
125
125
  Bolt::Logger.configure(config.log, config.color)
126
126
 
127
- # Logger must be configured before checking path case, otherwise warnings will not display
127
+ # Logger must be configured before checking path case and project file, otherwise warnings will not display
128
128
  @config.check_path_case('modulepath', @config.modulepath)
129
+ @config.project.check_deprecated_file
129
130
 
130
131
  # Log the file paths for loaded config files
131
132
  config_loaded
@@ -257,13 +258,11 @@ module Bolt
257
258
  end
258
259
 
259
260
  def puppetdb_client
260
- return @puppetdb_client if @puppetdb_client
261
- puppetdb_config = Bolt::PuppetDB::Config.load_config(nil, config.puppetdb, config.project.path)
262
- @puppetdb_client = Bolt::PuppetDB::Client.new(puppetdb_config)
261
+ plugins.puppetdb_client
263
262
  end
264
263
 
265
264
  def plugins
266
- @plugins ||= Bolt::Plugin.setup(config, pal, puppetdb_client, analytics)
265
+ @plugins ||= Bolt::Plugin.setup(config, pal, analytics)
267
266
  end
268
267
 
269
268
  def query_puppetdb_nodes(query)
@@ -538,6 +537,17 @@ module Bolt
538
537
  Puppet[:tasks] = false
539
538
  ast = pal.parse_manifest(code, filename)
540
539
 
540
+ if defined?(ast.body) &&
541
+ (ast.body.is_a?(Puppet::Pops::Model::HostClassDefinition) ||
542
+ ast.body.is_a?(Puppet::Pops::Model::ResourceTypeDefinition))
543
+ message = "Manifest only contains definitions and will result in no changes on the targets. "\
544
+ "Definitions must be declared for their resources to be applied. You can read more "\
545
+ "about defining and declaring classes and types in the Puppet documentation at "\
546
+ "https://puppet.com/docs/puppet/latest/lang_classes.html and "\
547
+ "https://puppet.com/docs/puppet/latest/lang_defined_types.html"
548
+ @logger.warn(message)
549
+ end
550
+
541
551
  executor = Bolt::Executor.new(config.concurrency, analytics, noop)
542
552
  executor.subscribe(outputter) if options.fetch(:format, 'human') == 'human'
543
553
  executor.subscribe(log_outputter)
@@ -47,9 +47,7 @@ module Bolt
47
47
  "targets on the command line and from plans.",
48
48
  "log" => "The configuration of the logfile output. Configuration can be set for "\
49
49
  "`console` and the path to a log file, such as `~/.puppetlabs/bolt/debug.log`.",
50
- "modulepath" => "The module path for loading tasks and plan code. This is either an array "\
51
- "of directories or a string containing a list of directories separated by the "\
52
- "OS-specific PATH separator.",
50
+ "modulepath" => "An array of directories that Bolt loads content (e.g. plans and tasks) from.",
53
51
  "plugin_hooks" => "Which plugins a specific hook should use.",
54
52
  "plugins" => "A map of plugins and their configuration data.",
55
53
  "puppetdb" => "A map containing options for configuring the Bolt PuppetDB client.",
@@ -66,8 +64,8 @@ module Bolt
66
64
 
67
65
  DEFAULT_OPTIONS = {
68
66
  "color" => true,
69
- "concurrency" => 100,
70
67
  "compile-concurrency" => "Number of cores",
68
+ "concurrency" => "100 or one-third of the ulimit, whichever is lower",
71
69
  "format" => "human",
72
70
  "hiera-config" => "Boltdir/hiera.yaml",
73
71
  "inventoryfile" => "Boltdir/inventory.yaml",
@@ -103,6 +101,8 @@ module Bolt
103
101
  "show_diff" => false
104
102
  }.freeze
105
103
 
104
+ DEFAULT_DEFAULT_CONCURRENCY = 100
105
+
106
106
  def self.default
107
107
  new(Bolt::Project.new('.'), {})
108
108
  end
@@ -113,7 +113,7 @@ module Bolt
113
113
  data: Bolt::Util.read_optional_yaml_hash(project.config_file, 'config')
114
114
  }
115
115
 
116
- data = load_defaults.push(data).select { |config| config[:data]&.any? }
116
+ data = load_defaults(project).push(data).select { |config| config[:data]&.any? }
117
117
 
118
118
  new(project, data, overrides)
119
119
  end
@@ -125,27 +125,29 @@ module Bolt
125
125
  filepath: project.config_file,
126
126
  data: Bolt::Util.read_yaml_hash(configfile, 'config')
127
127
  }
128
- data = load_defaults.push(data).select { |config| config[:data]&.any? }
128
+ data = load_defaults(project).push(data).select { |config| config[:data]&.any? }
129
129
 
130
130
  new(project, data, overrides)
131
131
  end
132
132
 
133
- def self.load_defaults
133
+ def self.load_defaults(project)
134
134
  # Lazy-load expensive gem code
135
135
  require 'win32/dir' if Bolt::Util.windows?
136
136
 
137
- system_path = if Bolt::Util.windows?
138
- Pathname.new(File.join(Dir::COMMON_APPDATA, 'PuppetLabs', 'bolt', 'etc', 'bolt.yaml'))
139
- else
140
- Pathname.new(File.join('/etc', 'puppetlabs', 'bolt', 'bolt.yaml'))
141
- end
137
+ # Don't load /etc/puppetlabs/bolt/bolt.yaml twice
138
+ confs = if project.path == Bolt::Project.system_path
139
+ []
140
+ else
141
+ system_path = Pathname.new(File.join(Bolt::Project.system_path, 'bolt.yaml'))
142
+ [{ filepath: system_path, data: Bolt::Util.read_optional_yaml_hash(system_path, 'config') }]
143
+ end
144
+
142
145
  user_path = begin
143
146
  Pathname.new(File.expand_path(File.join('~', '.puppetlabs', 'etc', 'bolt', 'bolt.yaml')))
144
147
  rescue ArgumentError
145
148
  nil
146
149
  end
147
150
 
148
- confs = [{ filepath: system_path, data: Bolt::Util.read_optional_yaml_hash(system_path, 'config') }]
149
151
  confs << { filepath: user_path, data: Bolt::Util.read_optional_yaml_hash(user_path, 'config') } if user_path
150
152
  confs
151
153
  end
@@ -165,7 +167,7 @@ module Bolt
165
167
  'apply_settings' => {},
166
168
  'color' => true,
167
169
  'compile-concurrency' => Etc.nprocessors,
168
- 'concurrency' => 100,
170
+ 'concurrency' => default_concurrency,
169
171
  'format' => 'human',
170
172
  'log' => { 'console' => {} },
171
173
  'plugin_hooks' => {},
@@ -183,6 +185,18 @@ module Bolt
183
185
 
184
186
  override_data = normalize_overrides(overrides)
185
187
 
188
+ # If we need to lower concurrency and concurrency is not configured
189
+ ld_concurrency = loaded_data.map(&:keys).flatten.include?('concurrency')
190
+ if default_concurrency != DEFAULT_DEFAULT_CONCURRENCY &&
191
+ !ld_concurrency &&
192
+ !override_data.key?('concurrency')
193
+ concurrency_warning = { option: 'concurrency',
194
+ msg: "Concurrency will default to #{default_concurrency} because ulimit "\
195
+ "is low: #{Etc.sysconf(Etc::SC_OPEN_MAX)}. Set concurrency with "\
196
+ "'--concurrency', or set your ulimit with 'ulimit -n <limit>'" }
197
+ @warnings << concurrency_warning
198
+ end
199
+
186
200
  @data = merge_config_layers(default_data, *loaded_data, override_data)
187
201
 
188
202
  TRANSPORT_CONFIG.each do |transport, config|
@@ -312,10 +326,10 @@ module Bolt
312
326
  @warnings << { option: 'future', msg: msg }
313
327
  end
314
328
 
315
- keys = OPTIONS.keys - %w[plugins plugin_hooks]
329
+ keys = OPTIONS.keys - %w[plugins plugin_hooks puppetdb]
316
330
  keys.each do |key|
317
331
  next unless Bolt::Util.references?(@data[key])
318
- valid_keys = TRANSPORT_CONFIG.keys + %w[plugins plugin_hooks]
332
+ valid_keys = TRANSPORT_CONFIG.keys + %w[plugins plugin_hooks puppetdb]
319
333
  raise Bolt::ValidationError,
320
334
  "Found unsupported key _plugin in config setting #{key}. Plugins are only available in "\
321
335
  "#{valid_keys.join(', ')}."
@@ -458,5 +472,19 @@ module Bolt
458
472
  l =~ /[A-Za-z]/ ? "[#{l.upcase}#{l.downcase}]" : l
459
473
  end.join
460
474
  end
475
+
476
+ # Etc::SC_OPEN_MAX is meaningless on windows, not defined in PE Jruby and not available
477
+ # on some platforms. This method holds the logic to decide whether or not to even consider it.
478
+ def sc_open_max_available?
479
+ !Bolt::Util.windows? && defined?(Etc::SC_OPEN_MAX) && Etc.sysconf(Etc::SC_OPEN_MAX)
480
+ end
481
+
482
+ def default_concurrency
483
+ @default_concurrency ||= if !sc_open_max_available? || Etc.sysconf(Etc::SC_OPEN_MAX) >= 300
484
+ DEFAULT_DEFAULT_CONCURRENCY
485
+ else
486
+ Etc.sysconf(Etc::SC_OPEN_MAX) / 3
487
+ end
488
+ end
461
489
  end
462
490
  end
@@ -13,14 +13,20 @@ module Bolt
13
13
  # in schemas/bolt-transport-definitions.json
14
14
  OPTIONS = {
15
15
  "cleanup" => { type: TrueClass,
16
+ external: true,
16
17
  desc: "Whether to clean up temporary files created on targets." },
17
18
  "connect-timeout" => { type: Integer,
18
19
  desc: "How long to wait when establishing connections." },
20
+ "copy-command" => { external: true,
21
+ desc: "Command to use when copying files using ssh-command. "\
22
+ "Bolt runs `<copy-command> <src> <dest>`. **This option is experimental.**" },
19
23
  "disconnect-timeout" => { type: Integer,
20
24
  desc: "How long to wait before force-closing a connection." },
21
25
  "host" => { type: String,
26
+ external: true,
22
27
  desc: "Host name." },
23
28
  "host-key-check" => { type: TrueClass,
29
+ external: true,
24
30
  desc: "Whether to perform host key validation when connecting." },
25
31
  "extensions" => { type: Array,
26
32
  desc: "List of file extensions that are accepted for scripts or tasks on Windows. "\
@@ -29,6 +35,7 @@ module Bolt
29
35
  "a `.py` script runs with `python.exe`. The extensions `.ps1`, `.rb`, and "\
30
36
  "`.pp` are always allowed and run via hard-coded executables." },
31
37
  "interpreters" => { type: Hash,
38
+ external: true,
32
39
  desc: "A map of an extension name to the absolute path of an executable, "\
33
40
  "enabling you to override the shebang defined in a task executable. The "\
34
41
  "extension can optionally be specified with the `.` character (`.py` and "\
@@ -44,26 +51,36 @@ module Bolt
44
51
  "password" => { type: String,
45
52
  desc: "Login password." },
46
53
  "port" => { type: Integer,
54
+ external: true,
47
55
  desc: "Connection port." },
48
- "private-key" => { desc: "Either the path to the private key file to use for authentication, or a "\
56
+ "private-key" => { external: true,
57
+ desc: "Either the path to the private key file to use for authentication, or a "\
49
58
  "hash with the key `key-data` and the contents of the private key." },
50
59
  "proxyjump" => { type: String,
51
60
  desc: "A jump host to proxy connections through, and an optional user to "\
52
61
  "connect with." },
53
62
  "run-as" => { type: String,
63
+ external: true,
54
64
  desc: "A different user to run commands as after login." },
55
65
  "run-as-command" => { type: Array,
66
+ external: true,
56
67
  desc: "The command to elevate permissions. Bolt appends the user and command "\
57
68
  "strings to the configured `run-as-command` before running it on the "\
58
69
  "target. This command must not require an interactive password prompt, "\
59
70
  "and the `sudo-password` option is ignored when `run-as-command` is "\
60
71
  "specified. The `run-as-command` must be specified as an array." },
61
72
  "script-dir" => { type: String,
73
+ external: true,
62
74
  desc: "The subdirectory of the tmpdir to use in place of a randomized "\
63
75
  "subdirectory for uploading and executing temporary files on the "\
64
76
  "target. It's expected that this directory already exists as a subdir "\
65
77
  "of tmpdir, which is either configured or defaults to `/tmp`." },
78
+ "ssh-command" => { external: true,
79
+ desc: "Command and flags to use when SSHing. This enables the external "\
80
+ "SSH transport which shells out to the specified command. "\
81
+ "**This option is experimental.**" },
66
82
  "sudo-executable" => { type: String,
83
+ external: true,
67
84
  desc: "The executable to use when escalating to the configured `run-as` "\
68
85
  "user. This is useful when you want to escalate using the configured "\
69
86
  "`sudo-password`, since `run-as-command` does not use `sudo-password` "\
@@ -71,14 +88,17 @@ module Bolt
71
88
  "`<sudo-executable> -S -u <user> -p custom_bolt_prompt <command>`. "\
72
89
  "**This option is experimental.**" },
73
90
  "sudo-password" => { type: String,
91
+ external: true,
74
92
  desc: "Password to use when changing users via `run-as`." },
75
93
  "tmpdir" => { type: String,
94
+ external: true,
76
95
  desc: "The directory to upload and execute temporary files on the target." },
77
96
  "tty" => { type: TrueClass,
78
97
  desc: "Request a pseudo tty for the session. This option is generally "\
79
98
  "only used in conjunction with the `run-as` option when the sudoers "\
80
99
  "policy requires a `tty`." },
81
100
  "user" => { type: String,
101
+ external: true,
82
102
  desc: "Login user." }
83
103
  }.freeze
84
104
 
@@ -103,6 +123,13 @@ module Bolt
103
123
 
104
124
  if key_opt.instance_of?(String)
105
125
  @config['private-key'] = File.expand_path(key_opt, @project)
126
+
127
+ # We have an explicit test for this to only warn if using net-ssh transport
128
+ Bolt::Util.validate_file('ssh key', @config['private-key']) if @config['ssh-command']
129
+ end
130
+
131
+ if key_opt.instance_of?(Hash) && @config['ssh-command']
132
+ raise Bolt::ValidationError, 'private-key must be a filepath when using ssh-command'
106
133
  end
107
134
  end
108
135
 
@@ -130,6 +157,25 @@ module Bolt
130
157
  end
131
158
  end
132
159
  end
160
+
161
+ if @config['ssh-command'] && !@config['load-config']
162
+ msg = 'Cannot use external SSH transport with load-config set to false'
163
+ raise Bolt::ValidationError, msg
164
+ end
165
+
166
+ if (ssh_cmd = @config['ssh-command'])
167
+ unless ssh_cmd.is_a?(String) || ssh_cmd.is_a?(Array)
168
+ raise Bolt::ValidationError,
169
+ "ssh-command must be a String or Array, received #{ssh_cmd.class} #{ssh_cmd.inspect}"
170
+ end
171
+ end
172
+
173
+ if (copy_cmd = @config['copy-command'])
174
+ unless copy_cmd.is_a?(String) || copy_cmd.is_a?(Array)
175
+ raise Bolt::ValidationError,
176
+ "copy-command must be a String or Array, received #{copy_cmd.class} #{copy_cmd.inspect}"
177
+ end
178
+ end
133
179
  end
134
180
  end
135
181
  end