bolt 2.6.0 → 2.11.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 (82) hide show
  1. checksums.yaml +4 -4
  2. data/Puppetfile +4 -3
  3. data/bolt-modules/boltlib/lib/puppet/datatypes/applyresult.rb +2 -0
  4. data/bolt-modules/boltlib/lib/puppet/datatypes/resourceinstance.rb +27 -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/run_plan.rb +1 -1
  9. data/bolt-modules/boltlib/lib/puppet/functions/run_task_with.rb +192 -0
  10. data/bolt-modules/boltlib/lib/puppet/functions/set_resources.rb +122 -0
  11. data/bolt-modules/boltlib/types/planresult.pp +12 -1
  12. data/bolt-modules/file/lib/puppet/functions/file/exists.rb +3 -1
  13. data/bolt-modules/file/lib/puppet/functions/file/join.rb +1 -1
  14. data/bolt-modules/file/lib/puppet/functions/file/read.rb +2 -1
  15. data/bolt-modules/file/lib/puppet/functions/file/readable.rb +3 -1
  16. data/bolt-modules/file/lib/puppet/functions/file/write.rb +3 -1
  17. data/bolt-modules/prompt/lib/puppet/functions/prompt.rb +43 -0
  18. data/lib/bolt/analytics.rb +1 -1
  19. data/lib/bolt/applicator.rb +3 -2
  20. data/lib/bolt/apply_inventory.rb +1 -1
  21. data/lib/bolt/apply_result.rb +1 -1
  22. data/lib/bolt/apply_target.rb +11 -2
  23. data/lib/bolt/bolt_option_parser.rb +27 -7
  24. data/lib/bolt/catalog.rb +32 -3
  25. data/lib/bolt/cli.rb +52 -22
  26. data/lib/bolt/config.rb +51 -27
  27. data/lib/bolt/config/transport/base.rb +3 -3
  28. data/lib/bolt/config/transport/docker.rb +7 -1
  29. data/lib/bolt/config/transport/local.rb +9 -1
  30. data/lib/bolt/config/transport/orch.rb +4 -2
  31. data/lib/bolt/config/transport/remote.rb +2 -0
  32. data/lib/bolt/config/transport/ssh.rb +81 -3
  33. data/lib/bolt/config/transport/winrm.rb +6 -1
  34. data/lib/bolt/executor.rb +38 -0
  35. data/lib/bolt/inventory.rb +2 -1
  36. data/lib/bolt/inventory/group.rb +1 -0
  37. data/lib/bolt/inventory/inventory.rb +9 -0
  38. data/lib/bolt/inventory/target.rb +17 -1
  39. data/lib/bolt/node/output.rb +1 -1
  40. data/lib/bolt/outputter/human.rb +5 -4
  41. data/lib/bolt/outputter/json.rb +1 -1
  42. data/lib/bolt/pal.rb +32 -14
  43. data/lib/bolt/pal/yaml_plan.rb +1 -0
  44. data/lib/bolt/plugin.rb +14 -8
  45. data/lib/bolt/plugin/env_var.rb +2 -1
  46. data/lib/bolt/plugin/module.rb +40 -7
  47. data/lib/bolt/plugin/prompt.rb +1 -1
  48. data/lib/bolt/plugin/puppetdb.rb +5 -2
  49. data/lib/bolt/project.rb +135 -0
  50. data/lib/bolt/puppetdb/config.rb +16 -28
  51. data/lib/bolt/rerun.rb +1 -1
  52. data/lib/bolt/resource_instance.rb +126 -0
  53. data/lib/bolt/result.rb +46 -23
  54. data/lib/bolt/result_set.rb +2 -5
  55. data/lib/bolt/secret.rb +20 -4
  56. data/lib/bolt/shell/bash.rb +27 -14
  57. data/lib/bolt/shell/bash/tmpdir.rb +1 -1
  58. data/lib/bolt/shell/powershell.rb +43 -15
  59. data/lib/bolt/shell/powershell/snippets.rb +1 -1
  60. data/lib/bolt/target.rb +18 -2
  61. data/lib/bolt/transport/base.rb +24 -8
  62. data/lib/bolt/transport/docker.rb +3 -3
  63. data/lib/bolt/transport/docker/connection.rb +11 -7
  64. data/lib/bolt/transport/local/connection.rb +13 -7
  65. data/lib/bolt/transport/orch.rb +5 -1
  66. data/lib/bolt/transport/ssh.rb +6 -2
  67. data/lib/bolt/transport/ssh/connection.rb +26 -1
  68. data/lib/bolt/transport/ssh/exec_connection.rb +110 -0
  69. data/lib/bolt/transport/winrm/connection.rb +10 -2
  70. data/lib/bolt/version.rb +1 -1
  71. data/lib/bolt_server/pe/pal.rb +1 -38
  72. data/lib/bolt_server/transport_app.rb +7 -7
  73. data/lib/bolt_spec/bolt_context.rb +3 -6
  74. data/lib/bolt_spec/plans.rb +78 -8
  75. data/lib/bolt_spec/plans/action_stubs.rb +37 -7
  76. data/lib/bolt_spec/plans/action_stubs/plan_stub.rb +55 -0
  77. data/lib/bolt_spec/plans/mock_executor.rb +62 -2
  78. data/lib/bolt_spec/run.rb +10 -13
  79. metadata +26 -7
  80. data/lib/bolt/boltdir.rb +0 -54
  81. data/lib/bolt/plugin/pkcs7.rb +0 -104
  82. data/lib/bolt/secret/base.rb +0 -41
@@ -3,7 +3,7 @@
3
3
  require 'etc'
4
4
  require 'logging'
5
5
  require 'pathname'
6
- require 'bolt/boltdir'
6
+ require 'bolt/project'
7
7
  require 'bolt/logger'
8
8
  require 'bolt/util'
9
9
  # Transport config objects
@@ -23,7 +23,7 @@ module Bolt
23
23
  end
24
24
 
25
25
  class Config
26
- attr_reader :config_files, :warnings, :data, :transports, :boltdir
26
+ attr_reader :config_files, :warnings, :data, :transports, :project
27
27
 
28
28
  TRANSPORT_CONFIG = {
29
29
  'ssh' => Bolt::Config::Transport::SSH,
@@ -34,6 +34,8 @@ module Bolt
34
34
  'remote' => Bolt::Config::Transport::Remote
35
35
  }.freeze
36
36
 
37
+ # NOTE: All configuration options should have a corresponding schema property
38
+ # in schemas/bolt-config.schema.json
37
39
  OPTIONS = {
38
40
  "apply_settings" => "A map of Puppet settings to use when applying Puppet code",
39
41
  "color" => "Whether to use colored output when printing messages to the console.",
@@ -64,8 +66,8 @@ module Bolt
64
66
 
65
67
  DEFAULT_OPTIONS = {
66
68
  "color" => true,
67
- "concurrency" => 100,
68
69
  "compile-concurrency" => "Number of cores",
70
+ "concurrency" => "100 or one-third of the ulimit, whichever is lower",
69
71
  "format" => "human",
70
72
  "hiera-config" => "Boltdir/hiera.yaml",
71
73
  "inventoryfile" => "Boltdir/inventory.yaml",
@@ -101,31 +103,33 @@ module Bolt
101
103
  "show_diff" => false
102
104
  }.freeze
103
105
 
106
+ DEFAULT_DEFAULT_CONCURRENCY = 100
107
+
104
108
  def self.default
105
- new(Bolt::Boltdir.new('.'), {})
109
+ new(Bolt::Project.new('.'), {})
106
110
  end
107
111
 
108
- def self.from_boltdir(boltdir, overrides = {})
112
+ def self.from_project(project, overrides = {})
109
113
  data = {
110
- filepath: boltdir.config_file,
111
- data: Bolt::Util.read_optional_yaml_hash(boltdir.config_file, 'config')
114
+ filepath: project.config_file,
115
+ data: Bolt::Util.read_optional_yaml_hash(project.config_file, 'config')
112
116
  }
113
117
 
114
118
  data = load_defaults.push(data).select { |config| config[:data]&.any? }
115
119
 
116
- new(boltdir, data, overrides)
120
+ new(project, data, overrides)
117
121
  end
118
122
 
119
123
  def self.from_file(configfile, overrides = {})
120
- boltdir = Bolt::Boltdir.new(Pathname.new(configfile).expand_path.dirname)
124
+ project = Bolt::Project.new(Pathname.new(configfile).expand_path.dirname)
121
125
 
122
126
  data = {
123
- filepath: boltdir.config_file,
127
+ filepath: project.config_file,
124
128
  data: Bolt::Util.read_yaml_hash(configfile, 'config')
125
129
  }
126
130
  data = load_defaults.push(data).select { |config| config[:data]&.any? }
127
131
 
128
- new(boltdir, data, overrides)
132
+ new(project, data, overrides)
129
133
  end
130
134
 
131
135
  def self.load_defaults
@@ -148,14 +152,14 @@ module Bolt
148
152
  confs
149
153
  end
150
154
 
151
- def initialize(boltdir, config_data, overrides = {})
155
+ def initialize(project, config_data, overrides = {})
152
156
  unless config_data.is_a?(Array)
153
- config_data = [{ filepath: boltdir.config_file, data: config_data }]
157
+ config_data = [{ filepath: project.config_file, data: config_data }]
154
158
  end
155
159
 
156
160
  @logger = Logging.logger[self]
157
161
  @warnings = []
158
- @boltdir = boltdir
162
+ @project = project
159
163
  @transports = {}
160
164
  @config_files = []
161
165
 
@@ -163,7 +167,7 @@ module Bolt
163
167
  'apply_settings' => {},
164
168
  'color' => true,
165
169
  'compile-concurrency' => Etc.nprocessors,
166
- 'concurrency' => 100,
170
+ 'concurrency' => default_concurrency,
167
171
  'format' => 'human',
168
172
  'log' => { 'console' => {} },
169
173
  'plugin_hooks' => {},
@@ -181,10 +185,22 @@ module Bolt
181
185
 
182
186
  override_data = normalize_overrides(overrides)
183
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
+
184
200
  @data = merge_config_layers(default_data, *loaded_data, override_data)
185
201
 
186
202
  TRANSPORT_CONFIG.each do |transport, config|
187
- @transports[transport] = config.new(@data.delete(transport), @boltdir.path)
203
+ @transports[transport] = config.new(@data.delete(transport), @project.path)
188
204
  end
189
205
 
190
206
  finalize_data
@@ -250,7 +266,7 @@ module Bolt
250
266
  @data['log'] = update_logs(@data['log'])
251
267
  end
252
268
 
253
- # Expand paths relative to the Boltdir. Any settings that came from the
269
+ # Expand paths relative to the project. Any settings that came from the
254
270
  # CLI will already be absolute, so the expand will be skipped.
255
271
  if @data.key?('modulepath')
256
272
  moduledirs = if data['modulepath'].is_a?(String)
@@ -259,12 +275,12 @@ module Bolt
259
275
  data['modulepath']
260
276
  end
261
277
  @data['modulepath'] = moduledirs.map do |moduledir|
262
- File.expand_path(moduledir, @boltdir.path)
278
+ File.expand_path(moduledir, @project.path)
263
279
  end
264
280
  end
265
281
 
266
282
  %w[hiera-config inventoryfile trusted-external-command].each do |opt|
267
- @data[opt] = File.expand_path(@data[opt], @boltdir.path) if @data.key?(opt)
283
+ @data[opt] = File.expand_path(@data[opt], @project.path) if @data.key?(opt)
268
284
  end
269
285
 
270
286
  # Filter hashes to only include valid options
@@ -275,7 +291,7 @@ module Bolt
275
291
  private def normalize_log(target)
276
292
  return target if target == 'console'
277
293
  target = target[5..-1] if target.start_with?('file:')
278
- 'file:' + File.expand_path(target, @boltdir.path)
294
+ 'file:' + File.expand_path(target, @project.path)
279
295
  end
280
296
 
281
297
  private def update_logs(logs)
@@ -310,10 +326,10 @@ module Bolt
310
326
  @warnings << { option: 'future', msg: msg }
311
327
  end
312
328
 
313
- keys = OPTIONS.keys - %w[plugins plugin_hooks]
329
+ keys = OPTIONS.keys - %w[plugins plugin_hooks puppetdb]
314
330
  keys.each do |key|
315
331
  next unless Bolt::Util.references?(@data[key])
316
- valid_keys = TRANSPORT_CONFIG.keys + %w[plugins plugin_hooks]
332
+ valid_keys = TRANSPORT_CONFIG.keys + %w[plugins plugin_hooks puppetdb]
317
333
  raise Bolt::ValidationError,
318
334
  "Found unsupported key _plugin in config setting #{key}. Plugins are only available in "\
319
335
  "#{valid_keys.join(', ')}."
@@ -348,23 +364,23 @@ module Bolt
348
364
  end
349
365
 
350
366
  def default_inventoryfile
351
- @boltdir.inventory_file
367
+ @project.inventory_file
352
368
  end
353
369
 
354
370
  def rerunfile
355
- @boltdir.rerunfile
371
+ @project.rerunfile
356
372
  end
357
373
 
358
374
  def hiera_config
359
- @data['hiera-config'] || @boltdir.hiera_config
375
+ @data['hiera-config'] || @project.hiera_config
360
376
  end
361
377
 
362
378
  def puppetfile
363
- @puppetfile || @boltdir.puppetfile
379
+ @puppetfile || @project.puppetfile
364
380
  end
365
381
 
366
382
  def modulepath
367
- @data['modulepath'] || @boltdir.modulepath
383
+ @data['modulepath'] || @project.modulepath
368
384
  end
369
385
 
370
386
  def modulepath=(value)
@@ -456,5 +472,13 @@ module Bolt
456
472
  l =~ /[A-Za-z]/ ? "[#{l.upcase}#{l.downcase}]" : l
457
473
  end.join
458
474
  end
475
+
476
+ def default_concurrency
477
+ if Bolt::Util.windows? || Etc.sysconf(Etc::SC_OPEN_MAX) >= 300 || Etc.sysconf(Etc::SC_OPEN_MAX).nil?
478
+ DEFAULT_DEFAULT_CONCURRENCY
479
+ else
480
+ (Etc.sysconf(Etc::SC_OPEN_MAX) / 3).floor
481
+ end
482
+ end
459
483
  end
460
484
  end
@@ -9,12 +9,12 @@ module Bolt
9
9
  class Base
10
10
  attr_reader :input
11
11
 
12
- def initialize(data = {}, boltdir = nil)
12
+ def initialize(data = {}, project = nil)
13
13
  assert_hash_or_config(data)
14
14
  @input = data
15
15
  @resolved = !Bolt::Util.references?(input)
16
16
  @config = resolved? ? Bolt::Util.deep_merge(defaults, filter(input)) : defaults
17
- @boltdir = boltdir
17
+ @project = project
18
18
 
19
19
  validate if resolved?
20
20
  end
@@ -62,7 +62,7 @@ module Bolt
62
62
  Bolt::Util.deep_merge(acc, layer_data)
63
63
  end
64
64
 
65
- self.class.new(merged, @boltdir)
65
+ self.class.new(merged, @project)
66
66
  end
67
67
 
68
68
  # Resolve any references in the input data, then remerge it with the defaults
@@ -7,7 +7,11 @@ module Bolt
7
7
  class Config
8
8
  module Transport
9
9
  class Docker < Base
10
+ # NOTE: All transport configuration options should have a corresponding schema definition
11
+ # in schemas/bolt-transport-definitions.json
10
12
  OPTIONS = {
13
+ "cleanup" => { type: TrueClass,
14
+ desc: "Whether to clean up temporary files created on targets." },
11
15
  "host" => { type: String,
12
16
  desc: "Host name." },
13
17
  "interpreters" => { type: Hash,
@@ -27,7 +31,9 @@ module Bolt
27
31
  desc: "Whether to enable tty on exec commands." }
28
32
  }.freeze
29
33
 
30
- DEFAULTS = {}.freeze
34
+ DEFAULTS = {
35
+ 'cleanup' => true
36
+ }.freeze
31
37
 
32
38
  private def validate
33
39
  super
@@ -7,7 +7,11 @@ module Bolt
7
7
  class Config
8
8
  module Transport
9
9
  class Local < Base
10
+ # NOTE: All transport configuration options should have a corresponding schema definition
11
+ # in schemas/bolt-transport-definitions.json
10
12
  OPTIONS = {
13
+ "cleanup" => { type: TrueClass,
14
+ desc: "Whether to clean up temporary files created on targets." },
11
15
  "interpreters" => { type: Hash,
12
16
  desc: "A map of an extension name to the absolute path of an executable, "\
13
17
  "enabling you to override the shebang defined in a task executable. The "\
@@ -36,6 +40,8 @@ module Bolt
36
40
  }.freeze
37
41
 
38
42
  WINDOWS_OPTIONS = {
43
+ "cleanup" => { type: TrueClass,
44
+ desc: "Whether to clean up temporary files created on targets." },
39
45
  "interpreters" => { type: Hash,
40
46
  desc: "A map of an extension name to the absolute path of an executable, "\
41
47
  "enabling you to override the shebang defined in a task executable. The "\
@@ -47,7 +53,9 @@ module Bolt
47
53
  desc: "The directory to copy and execute temporary files." }
48
54
  }.freeze
49
55
 
50
- DEFAULTS = {}.freeze
56
+ DEFAULTS = {
57
+ 'cleanup' => true
58
+ }.freeze
51
59
 
52
60
  def self.options
53
61
  Bolt::Util.windows? ? WINDOWS_OPTIONS : OPTIONS
@@ -7,6 +7,8 @@ module Bolt
7
7
  class Config
8
8
  module Transport
9
9
  class Orch < Base
10
+ # NOTE: All transport configuration options should have a corresponding schema definition
11
+ # in schemas/bolt-transport-definitions.json
10
12
  OPTIONS = {
11
13
  "cacert" => { type: String,
12
14
  desc: "The path to the CA certificate." },
@@ -32,12 +34,12 @@ module Bolt
32
34
  super
33
35
 
34
36
  if @config['cacert']
35
- @config['cacert'] = File.expand_path(@config['cacert'], @boltdir)
37
+ @config['cacert'] = File.expand_path(@config['cacert'], @project)
36
38
  Bolt::Util.validate_file('cacert', @config['cacert'])
37
39
  end
38
40
 
39
41
  if @config['token-file']
40
- @config['token-file'] = File.expand_path(@config['token-file'], @boltdir)
42
+ @config['token-file'] = File.expand_path(@config['token-file'], @project)
41
43
  Bolt::Util.validate_file('token-file', @config['token-file'])
42
44
  end
43
45
  end
@@ -7,6 +7,8 @@ module Bolt
7
7
  class Config
8
8
  module Transport
9
9
  class Remote < Base
10
+ # NOTE: All transport configuration options should have a corresponding schema definition
11
+ # in schemas/bolt-transport-definitions.json
10
12
  OPTIONS = {
11
13
  "run-on" => { type: String,
12
14
  desc: "The proxy target that the task executes on." }
@@ -7,16 +7,35 @@ module Bolt
7
7
  class Config
8
8
  module Transport
9
9
  class SSH < Base
10
+ LOGIN_SHELLS = %w[sh bash zsh dash ksh powershell].freeze
11
+
12
+ # NOTE: All transport configuration options should have a corresponding schema definition
13
+ # in schemas/bolt-transport-definitions.json
10
14
  OPTIONS = {
15
+ "cleanup" => { type: TrueClass,
16
+ external: true,
17
+ desc: "Whether to clean up temporary files created on targets." },
11
18
  "connect-timeout" => { type: Integer,
12
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.**" },
13
23
  "disconnect-timeout" => { type: Integer,
14
24
  desc: "How long to wait before force-closing a connection." },
15
25
  "host" => { type: String,
26
+ external: true,
16
27
  desc: "Host name." },
17
28
  "host-key-check" => { type: TrueClass,
29
+ external: true,
18
30
  desc: "Whether to perform host key validation when connecting." },
31
+ "extensions" => { type: Array,
32
+ desc: "List of file extensions that are accepted for scripts or tasks on Windows. "\
33
+ "Scripts with these file extensions rely on the target's file type "\
34
+ "association to run. For example, if Python is installed on the system, "\
35
+ "a `.py` script runs with `python.exe`. The extensions `.ps1`, `.rb`, and "\
36
+ "`.pp` are always allowed and run via hard-coded executables." },
19
37
  "interpreters" => { type: Hash,
38
+ external: true,
20
39
  desc: "A map of an extension name to the absolute path of an executable, "\
21
40
  "enabling you to override the shebang defined in a task executable. The "\
22
41
  "extension can optionally be specified with the `.` character (`.py` and "\
@@ -25,29 +44,43 @@ module Bolt
25
44
  "Bolt Ruby interpreter by default." },
26
45
  "load-config" => { type: TrueClass,
27
46
  desc: "Whether to load system SSH configuration." },
47
+ "login-shell" => { type: String,
48
+ desc: "Which login shell Bolt should expect on the target. "\
49
+ "Supported shells are #{LOGIN_SHELLS.join(', ')}. "\
50
+ "**This option is experimental.**" },
28
51
  "password" => { type: String,
29
52
  desc: "Login password." },
30
53
  "port" => { type: Integer,
54
+ external: true,
31
55
  desc: "Connection port." },
32
- "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 "\
33
58
  "hash with the key `key-data` and the contents of the private key." },
34
59
  "proxyjump" => { type: String,
35
60
  desc: "A jump host to proxy connections through, and an optional user to "\
36
61
  "connect with." },
37
62
  "run-as" => { type: String,
63
+ external: true,
38
64
  desc: "A different user to run commands as after login." },
39
65
  "run-as-command" => { type: Array,
66
+ external: true,
40
67
  desc: "The command to elevate permissions. Bolt appends the user and command "\
41
68
  "strings to the configured `run-as-command` before running it on the "\
42
69
  "target. This command must not require an interactive password prompt, "\
43
70
  "and the `sudo-password` option is ignored when `run-as-command` is "\
44
71
  "specified. The `run-as-command` must be specified as an array." },
45
72
  "script-dir" => { type: String,
73
+ external: true,
46
74
  desc: "The subdirectory of the tmpdir to use in place of a randomized "\
47
75
  "subdirectory for uploading and executing temporary files on the "\
48
76
  "target. It's expected that this directory already exists as a subdir "\
49
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.**" },
50
82
  "sudo-executable" => { type: String,
83
+ external: true,
51
84
  desc: "The executable to use when escalating to the configured `run-as` "\
52
85
  "user. This is useful when you want to escalate using the configured "\
53
86
  "`sudo-password`, since `run-as-command` does not use `sudo-password` "\
@@ -55,22 +88,27 @@ module Bolt
55
88
  "`<sudo-executable> -S -u <user> -p custom_bolt_prompt <command>`. "\
56
89
  "**This option is experimental.**" },
57
90
  "sudo-password" => { type: String,
91
+ external: true,
58
92
  desc: "Password to use when changing users via `run-as`." },
59
93
  "tmpdir" => { type: String,
94
+ external: true,
60
95
  desc: "The directory to upload and execute temporary files on the target." },
61
96
  "tty" => { type: TrueClass,
62
97
  desc: "Request a pseudo tty for the session. This option is generally "\
63
98
  "only used in conjunction with the `run-as` option when the sudoers "\
64
99
  "policy requires a `tty`." },
65
100
  "user" => { type: String,
101
+ external: true,
66
102
  desc: "Login user." }
67
103
  }.freeze
68
104
 
69
105
  DEFAULTS = {
106
+ "cleanup" => true,
70
107
  "connect-timeout" => 10,
71
108
  "tty" => false,
72
109
  "load-config" => true,
73
- "disconnect-timeout" => 5
110
+ "disconnect-timeout" => 5,
111
+ "login-shell" => 'bash'
74
112
  }.freeze
75
113
 
76
114
  private def validate
@@ -84,7 +122,14 @@ module Bolt
84
122
  end
85
123
 
86
124
  if key_opt.instance_of?(String)
87
- @config['private-key'] = File.expand_path(key_opt, @boltdir)
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'
88
133
  end
89
134
  end
90
135
 
@@ -92,12 +137,45 @@ module Bolt
92
137
  @config['interpreters'] = normalize_interpreters(@config['interpreters'])
93
138
  end
94
139
 
140
+ if @config['login-shell'] && !LOGIN_SHELLS.include?(@config['login-shell'])
141
+ raise Bolt::ValidationError,
142
+ "Unsupported login-shell #{@config['login-shell']}. Supported shells are #{LOGIN_SHELLS.join(', ')}"
143
+ end
144
+
95
145
  if (run_as_cmd = @config['run-as-command'])
96
146
  unless run_as_cmd.all? { |n| n.is_a?(String) }
97
147
  raise Bolt::ValidationError,
98
148
  "run-as-command must be an Array of Strings, received #{run_as_cmd.class} #{run_as_cmd.inspect}"
99
149
  end
100
150
  end
151
+
152
+ if @config['login-shell'] == 'powershell'
153
+ %w[tty run-as].each do |key|
154
+ if @config[key]
155
+ raise Bolt::ValidationError,
156
+ "#{key} is not supported when using PowerShell"
157
+ end
158
+ end
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
101
179
  end
102
180
  end
103
181
  end