bolt 1.42.0 → 1.43.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 (37) hide show
  1. checksums.yaml +4 -4
  2. data/bolt-modules/boltlib/lib/puppet/datatypes/target.rb +6 -5
  3. data/bolt-modules/boltlib/lib/puppet/functions/catch_errors.rb +4 -4
  4. data/bolt-modules/boltlib/lib/puppet/functions/get_target.rb +2 -3
  5. data/bolt-modules/boltlib/lib/puppet/functions/get_targets.rb +2 -3
  6. data/bolt-modules/boltlib/lib/puppet/functions/run_plan.rb +73 -13
  7. data/bolt-modules/boltlib/lib/puppet/functions/without_default_logging.rb +2 -2
  8. data/lib/bolt/applicator.rb +67 -2
  9. data/lib/bolt/apply_inventory.rb +89 -0
  10. data/lib/bolt/apply_result.rb +7 -0
  11. data/lib/bolt/apply_target.rb +77 -0
  12. data/lib/bolt/bolt_option_parser.rb +5 -1
  13. data/lib/bolt/catalog.rb +20 -5
  14. data/lib/bolt/config.rb +1 -1
  15. data/lib/bolt/error.rb +2 -1
  16. data/lib/bolt/executor.rb +4 -0
  17. data/lib/bolt/outputter/human.rb +3 -3
  18. data/lib/bolt/outputter/json.rb +11 -3
  19. data/lib/bolt/result.rb +18 -0
  20. data/lib/bolt/result_set.rb +12 -0
  21. data/lib/bolt/target.rb +1 -0
  22. data/lib/bolt/transport/local.rb +1 -1
  23. data/lib/bolt/transport/local/shell.rb +2 -1
  24. data/lib/bolt/transport/ssh.rb +8 -2
  25. data/lib/bolt/transport/ssh/connection.rb +2 -2
  26. data/lib/bolt/transport/sudoable/connection.rb +3 -1
  27. data/lib/bolt/version.rb +1 -1
  28. data/modules/aggregate/lib/puppet/functions/aggregate/count.rb +1 -1
  29. data/modules/aggregate/lib/puppet/functions/aggregate/nodes.rb +1 -0
  30. data/modules/aggregate/lib/puppet/functions/aggregate/targets.rb +21 -0
  31. data/modules/aggregate/plans/count.pp +4 -4
  32. data/modules/aggregate/plans/nodes.pp +1 -0
  33. data/modules/aggregate/plans/targets.pp +35 -0
  34. data/modules/canary/lib/puppet/functions/canary/skip.rb +9 -9
  35. data/modules/canary/plans/init.pp +9 -9
  36. data/modules/puppetdb_fact/plans/init.pp +4 -4
  37. metadata +7 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d70e65622c7ae119fafb6212eda5fa66e517bf190f404d00f150d98efa616ce8
4
- data.tar.gz: 9fdfbbf45efc6b1a3ec4f0364dfaf2d2682de8a30c3ab17b7893db9091da3e9f
3
+ metadata.gz: 2c237cb6fec506c63071962c4d8cf16fafb0b196abe720c5e0af8b9d8d350f11
4
+ data.tar.gz: b7505bc572e3e123b265b869ed2386bbb784882365102a3b816ab1b5e5599ab1
5
5
  SHA512:
6
- metadata.gz: 3ba016ff367bf902b3a512ec7a6f3c46bac6fed6a16ef207400af12a741821a5aa5baa18b38e8dbbed201d555e71f88eafb38912e4acdb3e715362bb27ec8f51
7
- data.tar.gz: a37fa6e6f3a703d3d2b7b00c578884a617578800c190105bfbc6398a5febdca5475fa96737f82fd0677b22f174932bf20fb30271964a65f959ebc6cea4dfce6f
6
+ metadata.gz: '0178352d34985cdd7c2960a018ff4a502b813763df806e72dd21f302bf26b173eeaa54eb2b44092b340e82b682b141fca5f72ba6e11bda6475556983dcbcf0ee'
7
+ data.tar.gz: 4473441d248c6023b3fe87364439f6c32498b6a6ac71b61925116838ab72eab22a457e309189cac2d4a0ebd58b57e7ded0140c902dc2367c5f45d76cbba5932a
@@ -3,6 +3,7 @@
3
3
  Puppet::DataTypes.create_type('Target') do
4
4
  begin
5
5
  inventory = Puppet.lookup(:bolt_inventory)
6
+
6
7
  inventory_version = inventory.version
7
8
  if inventory_version != 1
8
9
  target_implementation_class = inventory.target_implementation_class
@@ -10,7 +11,7 @@ Puppet::DataTypes.create_type('Target') do
10
11
  rescue Puppet::Context::UndefinedBindingError
11
12
  inventory_version = 1
12
13
  end
13
- load_file('bolt/target')
14
+ require 'bolt/target'
14
15
 
15
16
  if inventory_version == 1
16
17
  interface <<-PUPPET
@@ -33,15 +34,15 @@ Puppet::DataTypes.create_type('Target') do
33
34
  attributes => {
34
35
  uri => { type => Optional[String[1]], kind => given_or_derived },
35
36
  name => { type => Optional[String[1]] , kind => given_or_derived },
37
+ safe_name => { type => Optional[String[1]], kind => given_or_derived },
36
38
  target_alias => { type => Optional[Variant[String[1], Array[String[1]]]], kind => given_or_derived },
37
39
  config => { type => Optional[Hash[String[1], Data]], kind => given_or_derived },
38
- vars => { type => Optional[Hash[String[1], Data]], kind => given_or_derived },
39
- facts => { type => Optional[Hash[String[1], Data]], kind => given_or_derived },
40
+ vars => { type => Optional[Hash[String[1], Data]], kind => given_or_derived },
41
+ facts => { type => Optional[Hash[String[1], Data]], kind => given_or_derived },
40
42
  features => { type => Optional[Array[String[1]]], kind => given_or_derived },
41
- plugin_hooks => { type => Optional[Hash[String[1], Data]], kind => given_or_derived }
43
+ plugin_hooks => { type => Optional[Hash[String[1], Data]], kind => given_or_derived }
42
44
  },
43
45
  functions => {
44
- safe_name => Callable[[], String[1]],
45
46
  host => Callable[[], Optional[String]],
46
47
  password => Callable[[], Optional[String[1]]],
47
48
  port => Callable[[], Optional[Integer]],
@@ -12,14 +12,14 @@ Puppet::Functions.create_function(:catch_errors) do
12
12
  # otherwise the result will be returned
13
13
  # @example Catch errors for a block
14
14
  # catch_errors() || {
15
- # run_command("whoami", $nodes)
16
- # run_command("adduser ryan", $nodes)
15
+ # run_command("whoami", $targets)
16
+ # run_command("adduser ryan", $targets)
17
17
  # }
18
18
  # @example Catch parse errors for a block of code
19
19
  # catch_errors(['bolt/parse-error']) || {
20
- # run_plan('canary', $nodes)
20
+ # run_plan('canary', $targets)
21
21
  # run_plan('other_plan)
22
- # apply($nodes) || {
22
+ # apply($targets) || {
23
23
  # notify { "Hello": }
24
24
  # }
25
25
  # }
@@ -4,10 +4,9 @@ require 'bolt/error'
4
4
 
5
5
  # Get a single target from inventory if it exists, otherwise create a new Target.
6
6
  #
7
- # **NOTE:** Calling `get_target` inside an `apply` block with a
8
- # version 2 inventory creates a new Target object.
9
- # `get_target('all')` returns an empty array.
7
+ # **NOTE:** Calling `get_target('all')` returns an empty array.
10
8
  # **NOTE:** Only compatible with inventory v2
9
+ # **NOTE:** Not available in apply block when `future` is true
11
10
  Puppet::Functions.create_function(:get_target) do
12
11
  # @param name A Target name.
13
12
  # @return A single target, either new or from inventory.
@@ -3,10 +3,9 @@
3
3
  require 'bolt/error'
4
4
 
5
5
  # Parses common ways of referring to targets and returns an array of Targets.
6
- #
7
- # **NOTE:** Calling `get_targets` inside an `apply` block with a
8
- # version 2 inventory creates a new Target object.
9
6
  # `get_targets('all')` returns an empty array.
7
+ #
8
+ # **NOTE:** Not available in apply block when `future` is true
10
9
  Puppet::Functions.create_function(:get_targets) do
11
10
  # @param names A pattern or array of patterns identifying a set of targets.
12
11
  # @return A list of unique Targets resolved from any target URIs and groups.
@@ -11,7 +11,7 @@ Puppet::Functions.create_function(:run_plan, Puppet::Functions::InternalFunction
11
11
  # @param args Arguments to the plan. Can also include additional options: '_catch_errors', '_run_as'.
12
12
  # @return [PlanResult] The result of running the plan. Undef if plan does not explicitly return results.
13
13
  # @example Run a plan
14
- # run_plan('canary', 'command' => 'false', 'nodes' => $targets, '_catch_errors' => true)
14
+ # run_plan('canary', 'command' => 'false', 'targets' => $targets, '_catch_errors' => true)
15
15
  dispatch :run_plan do
16
16
  scope_param
17
17
  param 'String', :plan_name
@@ -19,13 +19,22 @@ Puppet::Functions.create_function(:run_plan, Puppet::Functions::InternalFunction
19
19
  return_type 'Boltlib::PlanResult'
20
20
  end
21
21
 
22
- # Run a plan, specifying $nodes as a positional argument.
22
+ # Run a plan, specifying $nodes or $targets as a positional argument.
23
+ #
24
+ # When running a plan with a $nodes parameter, the second positional argument will always specify
25
+ # the $nodes parameter. When running a plan with a $targets parameter and no $nodes parameter, the
26
+ # second positional argument specifies the $targets parameter.
27
+ #
28
+ # Deprecation Warning: Starting with Bolt 2.0, a plan with both a $nodes and $targets parameter
29
+ # cannot specify either parameter using the second positional argument and will result in the plan
30
+ # failing to run.
31
+ #
23
32
  # @param plan_name The plan to run.
24
33
  # @param args Arguments to the plan. Can also include additional options: '_catch_errors', '_run_as'.
25
34
  # @param targets A pattern identifying zero or more targets. See {get_targets} for accepted patterns.
26
35
  # @return [PlanResult] The result of running the plan. Undef if plan does not explicitly return results.
27
36
  # @example Run a plan
28
- # run_plan('canary', $nodes, 'command' => 'false')
37
+ # run_plan('canary', $targets, 'command' => 'false')
29
38
  dispatch :run_plan_with_targetspec do
30
39
  scope_param
31
40
  param 'String', :plan_name
@@ -35,16 +44,14 @@ Puppet::Functions.create_function(:run_plan, Puppet::Functions::InternalFunction
35
44
  end
36
45
 
37
46
  def run_plan_with_targetspec(scope, plan_name, targets, args = {})
38
- unless args['nodes'].nil?
39
- raise ArgumentError,
40
- "A plan's 'nodes' parameter may be specified as the second positional argument to " \
41
- "run_plan(), but in that case 'nodes' must not be specified in the named arguments " \
42
- "hash."
43
- end
44
- run_plan(scope, plan_name, args.merge('nodes' => targets))
47
+ run_inner_plan(scope, plan_name, targets, args)
45
48
  end
46
49
 
47
50
  def run_plan(scope, plan_name, args = {})
51
+ run_inner_plan(scope, plan_name, nil, args)
52
+ end
53
+
54
+ def run_inner_plan(scope, plan_name, targets, args = {})
48
55
  unless Puppet[:tasks]
49
56
  raise Puppet::ParseErrorWithIssue
50
57
  .from_issue_and_stack(Bolt::PAL::Issues::PLAN_OPERATION_NOT_SUPPORTED_WHEN_COMPILING, action: 'run_plan')
@@ -87,10 +94,13 @@ Puppet::Functions.create_function(:run_plan, Puppet::Functions::InternalFunction
87
94
  # If a TargetSpec parameter is passed, ensure it is in inventory
88
95
  inventory = Puppet.lookup(:bolt_inventory)
89
96
 
97
+ param_types = closure.parameters.each_with_object({}) do |param, param_acc|
98
+ param_acc[param.name] = extract_parameter_types(param.type_expr)&.flatten
99
+ end
100
+
101
+ targets_to_param(targets, params, param_types, executor) if targets
102
+
90
103
  if inventory.version > 1
91
- param_types = closure.parameters.each_with_object({}) do |param, param_acc|
92
- param_acc[param.name] = extract_parameter_types(param.type_expr).flatten
93
- end
94
104
  params.each do |param, value|
95
105
  # Note the safe lookup operator is needed to handle case where a parameter is passed to a
96
106
  # plan that the plan is not expecting
@@ -159,4 +169,54 @@ Puppet::Functions.create_function(:run_plan, Puppet::Functions::InternalFunction
159
169
  extract_parameter_types(type_expr.element_type)
160
170
  end
161
171
  end
172
+
173
+ def targets_to_param(targets, params, param_types, executor)
174
+ nodes_param = param_types.include?('nodes')
175
+ targets_param = param_types['targets']&.any? { |p| p.match?(/TargetSpec/) }
176
+
177
+ # Both a 'TargetSpec $nodes' and 'TargetSpec $targets' parameter are present in the plan
178
+ # 1.x behavior: Populate $nodes and warn user that this will error in 2.x
179
+ # 2.x behavior: Error
180
+ if nodes_param && targets_param
181
+ # rubocop:disable Style/GlobalVars
182
+ if $future
183
+ raise ArgumentError,
184
+ "A plan with both a $nodes and $targets parameter cannot have either parameter specified " \
185
+ "as the second positional argument to run_plan()."
186
+ else
187
+ msg = <<~WARNING
188
+ Deprecation Warning: A plan with both a $nodes and $targets parameter can only specify
189
+ the $nodes parameter as the second positional argument to run_plan(). Starting in
190
+ Bolt 2.0, a plan with both a $nodes and $targets parameter will not be able to specify
191
+ either parameter as the second positional argument to run_plan() and will result in the
192
+ plan failing.
193
+ WARNING
194
+ executor.deprecation(msg)
195
+ end
196
+ # rubocop:enable Style/GlobalVars
197
+ end
198
+
199
+ # Always populate a $nodes parameter over $targets
200
+ if nodes_param
201
+ if params['nodes']
202
+ raise ArgumentError,
203
+ "A plan's 'nodes' parameter may be specified as the second positional argument to " \
204
+ "run_plan(), but in that case 'nodes' must not be specified in the named arguments " \
205
+ "hash."
206
+ end
207
+ params['nodes'] = targets
208
+ # If there is only a $targets parameter, then populate it
209
+ elsif targets_param
210
+ if params['targets']
211
+ raise ArgumentError,
212
+ "A plan's 'targets' parameter may be specified as the second positional argument to " \
213
+ "run_plan(), but in that case 'targets' must not be specified in the named arguments " \
214
+ "hash."
215
+ end
216
+ params['targets'] = targets
217
+ # If a plan has neither parameter, just fall back to $nodes
218
+ else
219
+ params['nodes'] = targets
220
+ end
221
+ end
162
222
  end
@@ -13,8 +13,8 @@ Puppet::Functions.create_function(:without_default_logging) do
13
13
  # @example Suppress default logging for a series of functions
14
14
  # without_default_logging() || {
15
15
  # notice("Deploying on ${nodes}")
16
- # get_targets($nodes).each |$node| {
17
- # run_task(deploy, $node)
16
+ # get_targets($targets).each |$target| {
17
+ # run_task(deploy, $target)
18
18
  # }
19
19
  # }
20
20
  dispatch :without_default_logging do
@@ -8,6 +8,7 @@ require 'open3'
8
8
  require 'bolt/error'
9
9
  require 'bolt/task'
10
10
  require 'bolt/apply_result'
11
+ require 'bolt/apply_target'
11
12
  require 'bolt/util/puppet_log_level'
12
13
 
13
14
  module Bolt
@@ -129,6 +130,49 @@ module Bolt
129
130
  JSON.parse(out)
130
131
  end
131
132
 
133
+ def future_compile(target, catalog_input)
134
+ trusted = Puppet::Context::TrustedInformation.new('local', target.name, {})
135
+ catalog_input[:target] = {
136
+ name: target.name,
137
+ facts: @inventory.facts(target).merge('bolt' => true),
138
+ variables: @inventory.vars(target),
139
+ trusted: trusted.to_h
140
+ }
141
+ # rubocop:disable Style/GlobalVars
142
+ catalog_input[:future] = $future
143
+ # rubocop:enable Style/GlobalVars
144
+
145
+ bolt_catalog_exe = File.join(libexec, 'bolt_catalog')
146
+ old_path = ENV['PATH']
147
+ ENV['PATH'] = "#{RbConfig::CONFIG['bindir']}#{File::PATH_SEPARATOR}#{old_path}"
148
+ out, err, stat = Open3.capture3('ruby', bolt_catalog_exe, 'compile', stdin_data: catalog_input.to_json)
149
+ ENV['PATH'] = old_path
150
+
151
+ # stderr may contain formatted logs from Puppet's logger or other errors.
152
+ # Print them in order, but handle them separately. Anything not a formatted log is assumed
153
+ # to be an error message.
154
+ logs = err.lines.map do |l|
155
+ begin
156
+ JSON.parse(l)
157
+ rescue StandardError
158
+ l
159
+ end
160
+ end
161
+ logs.each do |log|
162
+ if log.is_a?(String)
163
+ @logger.error(log.chomp)
164
+ else
165
+ log.map { |k, v| [k.to_sym, v] }.each do |level, msg|
166
+ bolt_level = Bolt::Util::PuppetLogLevel::MAPPING[level]
167
+ @logger.send(bolt_level, "#{target.name}: #{msg.chomp}")
168
+ end
169
+ end
170
+ end
171
+
172
+ raise(ApplyError, target.name) unless stat.success?
173
+ JSON.parse(out)
174
+ end
175
+
132
176
  def validate_hiera_config(hiera_config)
133
177
  if File.exist?(File.path(hiera_config))
134
178
  data = File.open(File.path(hiera_config), "r:UTF-8") { |f| YAML.safe_load(f.read, [Symbol]) }
@@ -155,7 +199,6 @@ module Bolt
155
199
  options = args[1].map { |k, v| [k.sub(/^_/, '').to_sym, v] }.to_h
156
200
  end
157
201
 
158
- # collect plan vars and merge them over target vars
159
202
  plan_vars = scope.to_hash(true, true)
160
203
  %w[trusted server_facts facts].each { |k| plan_vars.delete(k) }
161
204
 
@@ -179,11 +222,33 @@ module Bolt
179
222
  def apply_ast(raw_ast, targets, options, plan_vars = {})
180
223
  ast = Puppet::Pops::Serialization::ToDataConverter.convert(raw_ast, rich_data: true, symbol_to_string: true)
181
224
 
225
+ # rubocop:disable Style/GlobalVars
226
+ if $future
227
+ # Serialize as pcore for *Result* objects
228
+ plan_vars = Puppet::Pops::Serialization::ToDataConverter.convert(plan_vars,
229
+ rich_data: true,
230
+ symbol_as_string: true,
231
+ type_by_reference: true,
232
+ local_reference: false)
233
+ scope = {
234
+ code_ast: ast,
235
+ modulepath: @modulepath,
236
+ pdb_config: @pdb_client.config.to_hash,
237
+ hiera_config: @hiera_config,
238
+ plan_vars: plan_vars,
239
+ # This data isn't available on the target config hash
240
+ config: @inventory.config.transport_data_get
241
+ }
242
+ end
243
+ # rubocop:enable Style/GlobalVars
244
+
182
245
  r = @executor.log_action('apply catalog', targets) do
183
246
  futures = targets.map do |target|
184
247
  Concurrent::Future.execute(executor: @pool) do
185
248
  @executor.with_node_logging("Compiling manifest block", [target]) do
186
- compile(target, ast, plan_vars)
249
+ # rubocop:disable Style/GlobalVars
250
+ $future ? future_compile(target, scope) : compile(target, ast, plan_vars)
251
+ # rubocop:enable Style/GlobalVars
187
252
  end
188
253
  end
189
254
  end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bolt/boltdir'
4
+ require 'bolt/config'
5
+ require 'bolt/error'
6
+
7
+ module Bolt
8
+ class ApplyInventory
9
+ class InvalidFunctionCall < Bolt::Error
10
+ def initialize(function)
11
+ super("The function '#{function}' is not callable within an apply block",
12
+ 'bolt.inventory/invalid-function-call')
13
+ end
14
+ end
15
+
16
+ attr_reader :config_hash
17
+
18
+ def initialize(config_hash = {})
19
+ @config_hash = config_hash
20
+ @targets = {}
21
+ end
22
+
23
+ def create_apply_target(target)
24
+ @targets[target.name] = target
25
+ end
26
+
27
+ def validate
28
+ @groups.validate
29
+ end
30
+
31
+ def version
32
+ 2
33
+ end
34
+
35
+ def target_implementation_class
36
+ Bolt::ApplyTarget
37
+ end
38
+
39
+ def get_targets(*_params)
40
+ raise InvalidFunctionCall, 'get_targets'
41
+ end
42
+
43
+ def get_target(*_params)
44
+ raise InvalidFunctionCall, 'get_target'
45
+ end
46
+
47
+ # rubocop:disable Naming/AccessorMethodName
48
+ def set_var(*_params)
49
+ raise InvalidFunctionCall, 'set_var'
50
+ end
51
+
52
+ def set_feature(*_params)
53
+ raise InvalidFunctionCall, 'set_feature'
54
+ end
55
+ # rubocop:enable Naming/AccessorMethodName
56
+
57
+ def vars(target)
58
+ @targets[target.name].vars
59
+ end
60
+
61
+ def add_facts(*_params)
62
+ raise InvalidFunctionCall, 'add_facts'
63
+ end
64
+
65
+ def facts(target)
66
+ @targets[target.name].facts
67
+ end
68
+
69
+ def features(target)
70
+ @targets[target.name].features
71
+ end
72
+
73
+ def add_to_group(*_params)
74
+ raise InvalidFunctionCall, 'add_to_group'
75
+ end
76
+
77
+ def plugin_hooks(target)
78
+ @targets[target.name].plugin_hooks
79
+ end
80
+
81
+ def set_config(_target, _key_or_key_path, _value)
82
+ raise InvalidFunctionCall, 'set_config'
83
+ end
84
+
85
+ def target_config(target)
86
+ @targets[target.name].config
87
+ end
88
+ end
89
+ end
@@ -85,6 +85,13 @@ module Bolt
85
85
  end
86
86
  end
87
87
 
88
+ # Other pcore methods are inherited from Result
89
+ def _pcore_init_hash
90
+ { 'target' => @target,
91
+ 'error' => value['_error'],
92
+ 'report' => value['report'] }
93
+ end
94
+
88
95
  def initialize(target, error: nil, report: nil)
89
96
  @target = target
90
97
  @value = {}
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bolt
4
+ class ApplyTarget
5
+ ATTRIBUTES = %i[uri name target_alias config vars facts features
6
+ plugin_hooks safe_name].freeze
7
+ COMPUTED = %i[host password port protocol user].freeze
8
+
9
+ attr_reader(*ATTRIBUTES)
10
+ attr_accessor(*COMPUTED)
11
+
12
+ # rubocop:disable Lint/UnusedMethodArgument
13
+ # Target.new from a plan initialized with a hash
14
+ def self.from_asserted_hash(hash)
15
+ raise Bolt::Error.new("Target objects cannot be instantiated inside apply blocks", 'bolt/apply-error')
16
+ end
17
+
18
+ # Target.new from a plan with just a uri.
19
+ def self.from_asserted_args(uri = nil,
20
+ name = nil,
21
+ safe_name = nil,
22
+ target_alias = nil,
23
+ config = nil,
24
+ facts = nil,
25
+ vars = nil,
26
+ features = nil,
27
+ plugin_hooks = nil)
28
+ raise Bolt::Error.new("Target objects cannot be instantiated inside apply blocks", 'bolt/apply-error')
29
+ end
30
+ # rubocop:enable Lint/UnusedMethodArgument
31
+
32
+ def self._pcore_init_from_hash
33
+ raise "ApplyTarget shouldn't be instantiated from a pcore_init class method. How did this get called?"
34
+ end
35
+
36
+ def _pcore_init_from_hash(init_hash)
37
+ inventory = Puppet.lookup(:bolt_inventory)
38
+ initialize(init_hash, inventory.config_hash)
39
+ inventory.create_apply_target(self)
40
+ self
41
+ end
42
+
43
+ def initialize(target_hash, config)
44
+ ATTRIBUTES.each do |attr|
45
+ instance_variable_set("@#{attr}", target_hash[attr.to_s])
46
+ end
47
+
48
+ # Merge the config hash with inventory config
49
+ config = Bolt::Util.deep_merge(config, @config)
50
+ transport = config['transport'] || 'ssh'
51
+ t_conf = config['transports'][transport] || {}
52
+ uri_obj = parse_uri(uri)
53
+ @host = uri_obj.hostname || t_conf['host']
54
+ @password = Addressable::URI.unencode_component(uri_obj.password) || t_conf['password']
55
+ @port = uri_obj.port || t_conf['port']
56
+ @protocol = uri_obj.scheme || transport
57
+ @user = Addressable::URI.unencode_component(uri_obj.user) || t_conf['user']
58
+ end
59
+
60
+ def parse_uri(string)
61
+ require 'addressable/uri'
62
+ if string.nil?
63
+ Addressable::URI.new
64
+ # Forbid empty uri
65
+ elsif string.empty?
66
+ raise Bolt::ParseError, "Could not parse target URI: URI is empty string"
67
+ elsif string =~ %r{^[^:]+://}
68
+ Addressable::URI.parse(string)
69
+ else
70
+ # Initialize with an empty scheme to ensure we parse the hostname correctly
71
+ Addressable::URI.parse("//#{string}")
72
+ end
73
+ rescue Addressable::URI::InvalidURIError => e
74
+ raise Bolt::ParseError, "Could not parse target URI: #{e.message}"
75
+ end
76
+ end
77
+ end
@@ -8,7 +8,7 @@ module Bolt
8
8
  class BoltOptionParser < OptionParser
9
9
  OPTIONS = { inventory: %w[nodes targets query rerun description],
10
10
  authentication: %w[user password password-prompt private-key host-key-check ssl ssl-verify],
11
- escalation: %w[run-as sudo-password sudo-password-prompt],
11
+ escalation: %w[run-as sudo-password sudo-password-prompt sudo-executable],
12
12
  run_context: %w[concurrency inventoryfile save-rerun],
13
13
  global_config_setters: %w[modulepath boltdir configfile],
14
14
  transports: %w[transport connect-timeout tty],
@@ -703,6 +703,10 @@ module Bolt
703
703
  @options[:'sudo-password'] = STDIN.noecho(&:gets).chomp
704
704
  STDERR.puts
705
705
  end
706
+ define('--sudo-executable EXEC', "Specify an executable for running as another user.",
707
+ "This option is experimental.") do |exec|
708
+ @options[:'sudo-executable'] = exec
709
+ end
706
710
 
707
711
  separator "\nRUN CONTEXT OPTIONS"
708
712
  define('-c', '--concurrency CONCURRENCY', Integer,
@@ -1,7 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'bolt/apply_target'
3
4
  require 'bolt/config'
5
+ require 'bolt/error'
4
6
  require 'bolt/inventory'
7
+ require 'bolt/apply_inventory'
5
8
  require 'bolt/pal'
6
9
  require 'bolt/puppetdb'
7
10
  require 'bolt/util'
@@ -65,17 +68,29 @@ module Bolt
65
68
  target = request['target']
66
69
  pdb_client = Bolt::PuppetDB::Client.new(Bolt::PuppetDB::Config.new(request['pdb_config']))
67
70
  options = request['puppet_config'] || {}
71
+
68
72
  with_puppet_settings(request['hiera_config']) do
69
73
  Puppet[:rich_data] = true
70
74
  Puppet[:node_name_value] = target['name']
71
- Puppet::Pal.in_tmp_environment('bolt_catalog',
72
- modulepath: request["modulepath"] || [],
73
- facts: target["facts"] || {},
74
- variables: target["variables"] || {}) do |pal|
75
+ env_conf = { modulepath: request['modulepath'] || [],
76
+ facts: target['facts'] || {} }
77
+ env_conf[:variables] = request['future'] ? {} : target['variables']
78
+ Puppet::Pal.in_tmp_environment('bolt_catalog', env_conf) do |pal|
79
+ inv = if request['future']
80
+ Bolt::ApplyInventory.new(request['config'])
81
+ else
82
+ setup_inventory(request['inventory'])
83
+ end
75
84
  Puppet.override(bolt_pdb_client: pdb_client,
76
- bolt_inventory: setup_inventory(request['inventory'])) do
85
+ bolt_inventory: inv) do
77
86
  Puppet.lookup(:pal_current_node).trusted_data = target['trusted']
78
87
  pal.with_catalog_compiler do |compiler|
88
+ if request['future']
89
+ # This needs to happen inside the catalog compiler so loaders are initialized for loading
90
+ vars = Puppet::Pops::Serialization::FromDataConverter.convert(request['plan_vars'])
91
+ pal.send(:add_variables, compiler.send(:topscope), vars.merge(target['variables']))
92
+ end
93
+
79
94
  # Configure language strictness in the CatalogCompiler. We want Bolt to be able
80
95
  # to compile most Puppet 4+ manifests, so we default to allowing deprecated functions.
81
96
  Puppet[:strict] = options['strict'] || :warning
@@ -36,7 +36,7 @@ module Bolt
36
36
  :puppetfile_config, :plugins, :plugin_hooks, :future
37
37
  attr_writer :modulepath
38
38
 
39
- TRANSPORT_OPTIONS = %i[password run-as sudo-password extensions
39
+ TRANSPORT_OPTIONS = %i[password run-as sudo-password extensions sudo-executable
40
40
  private-key tty tmpdir user connect-timeout disconnect-timeout
41
41
  cacert token-file service-url interpreters file-protocol smb-port realm].freeze
42
42
 
@@ -61,7 +61,8 @@ module Bolt
61
61
  'result_set' => result_set
62
62
  }
63
63
  object_msg = " '#{object}'" if object
64
- message = "Plan aborted: #{action}#{object_msg} failed on #{result_set.error_set.length} nodes"
64
+ message = "Plan aborted: #{action}#{object_msg} failed on #{result_set.error_set.length} target"
65
+ message += "s" unless result_set.error_set.length == 1
65
66
  super(message, 'bolt/run-failure', details)
66
67
  @result_set = result_set
67
68
  @error_code = 2
@@ -338,5 +338,9 @@ module Bolt
338
338
  ensure
339
339
  publish_event(type: :enable_default_output)
340
340
  end
341
+
342
+ def deprecation(msg)
343
+ @logger.warn msg
344
+ end
341
345
  end
342
346
  end
@@ -167,7 +167,7 @@ module Bolt
167
167
  def print_summary(results, elapsed_time = nil)
168
168
  ok_set = results.ok_set
169
169
  unless ok_set.empty?
170
- @stream.puts format('Successful on %<size>d node%<plural>s: %<names>s',
170
+ @stream.puts format('Successful on %<size>d target%<plural>s: %<names>s',
171
171
  size: ok_set.size,
172
172
  plural: ok_set.size == 1 ? '' : 's',
173
173
  names: ok_set.targets.map(&:safe_name).join(','))
@@ -176,13 +176,13 @@ module Bolt
176
176
  error_set = results.error_set
177
177
  unless error_set.empty?
178
178
  @stream.puts colorize(:red,
179
- format('Failed on %<size>d node%<plural>s: %<names>s',
179
+ format('Failed on %<size>d target%<plural>s: %<names>s',
180
180
  size: error_set.size,
181
181
  plural: error_set.size == 1 ? '' : 's',
182
182
  names: error_set.targets.map(&:safe_name).join(',')))
183
183
  end
184
184
 
185
- total_msg = format('Ran on %<size>d node%<plural>s',
185
+ total_msg = format('Ran on %<size>d target%<plural>s',
186
186
  size: results.size,
187
187
  plural: results.size == 1 ? '' : 's')
188
188
  total_msg << " in #{duration_to_string(elapsed_time)}" unless elapsed_time.nil?
@@ -36,9 +36,17 @@ module Bolt
36
36
  @stream.puts "],\n"
37
37
  @preceding_item = false
38
38
  @items_open = false
39
- @stream.puts format('"node_count": %<size>d, "elapsed_time": %<elapsed>d }',
40
- size: results.size,
41
- elapsed: elapsed_time)
39
+ # rubocop:disable Style/GlobalVars
40
+ if $future
41
+ @stream.puts format('"target_count": %<size>d, "elapsed_time": %<elapsed>d }',
42
+ size: results.size,
43
+ elapsed: elapsed_time)
44
+ else
45
+ @stream.puts format('"node_count": %<size>d, "elapsed_time": %<elapsed>d }',
46
+ size: results.size,
47
+ elapsed: elapsed_time)
48
+ end
49
+ # rubocop:enable Style/GlobalVars
42
50
  end
43
51
 
44
52
  def print_table(results)
@@ -72,6 +72,24 @@ module Bolt
72
72
  new(target, value: value)
73
73
  end
74
74
 
75
+ def self._pcore_init_from_hash
76
+ raise "Result shouldn't be instantiated from a pcore_init class method. How did this get called?"
77
+ end
78
+
79
+ def _pcore_init_from_hash(init_hash)
80
+ opts = init_hash.reject { |k, _v| k == 'target' }
81
+ initialize(init_hash['target'], opts.map { |k, v| [k.to_sym, v] }.to_h)
82
+ end
83
+
84
+ def _pcore_init_hash
85
+ { 'target' => @target,
86
+ 'error' => @value['_error'],
87
+ 'message' => @value['_output'],
88
+ 'value' => @value,
89
+ 'action' => @action,
90
+ 'object' => @object }
91
+ end
92
+
75
93
  def initialize(target, error: nil, message: nil, value: nil, action: nil, object: nil)
76
94
  @target = target
77
95
  @value = value || {}
@@ -12,6 +12,18 @@ module Bolt
12
12
  include(Puppet::Pops::Types::IteratorProducer)
13
13
  end
14
14
 
15
+ def self._pcore_init_from_hash
16
+ raise "ResultSet shouldn't be instantiated from a pcore_init class method. How did this get called?"
17
+ end
18
+
19
+ def _pcore_init_from_hash(init_hash)
20
+ initialize(init_hash['results'])
21
+ end
22
+
23
+ def _pcore_init_hash
24
+ { 'results' => @results }
25
+ end
26
+
15
27
  def iterator
16
28
  if Object.const_defined?(:Puppet) && Puppet.const_defined?(:Pops) &&
17
29
  self.class.included_modules.include?(Puppet::Pops::Types::Iterable)
@@ -20,6 +20,7 @@ module Bolt
20
20
  # rubocop:disable Lint/UnusedMethodArgument
21
21
  def self.from_asserted_args(uri = nil,
22
22
  name = nil,
23
+ safe_name = nil,
23
24
  target_alias = nil,
24
25
  config = nil,
25
26
  facts = nil,
@@ -4,7 +4,7 @@ module Bolt
4
4
  module Transport
5
5
  class Local < Sudoable
6
6
  def self.options
7
- %w[tmpdir interpreters sudo-password run-as run-as-command]
7
+ %w[tmpdir interpreters sudo-password run-as run-as-command sudo-executable]
8
8
  end
9
9
 
10
10
  def provided_features
@@ -126,7 +126,8 @@ module Bolt
126
126
 
127
127
  if escalate
128
128
  if use_sudo
129
- sudo_flags = ["sudo", "-k", "-S", "-u", run_as, "-p", Sudoable.sudo_prompt]
129
+ sudo_exec = target.options['sudo-executable'] || "sudo"
130
+ sudo_flags = [sudo_exec, "-k", "-S", "-u", run_as, "-p", Sudoable.sudo_prompt]
130
131
  sudo_flags += ["-E"] if options[:environment]
131
132
  sudo_str = Shellwords.shelljoin(sudo_flags)
132
133
  else
@@ -9,8 +9,8 @@ module Bolt
9
9
  module Transport
10
10
  class SSH < Sudoable
11
11
  def self.options
12
- %w[host port user password sudo-password private-key host-key-check
13
- connect-timeout disconnect-timeout tmpdir run-as tty run-as-command proxyjump interpreters]
12
+ %w[host port user password sudo-password private-key host-key-check sudo-executable
13
+ connect-timeout disconnect-timeout tmpdir script-dir run-as tty run-as-command proxyjump interpreters]
14
14
  end
15
15
 
16
16
  def self.default_options
@@ -48,6 +48,12 @@ module Bolt
48
48
  raise Bolt::ValidationError, error_msg
49
49
  end
50
50
  end
51
+
52
+ if (dir_opt = options['script-dir'])
53
+ unless dir_opt.is_a?(String) && !dir_opt.empty?
54
+ raise Bolt::ValidationError, "script-dir option must be a non-empty string"
55
+ end
56
+ end
51
57
  end
52
58
 
53
59
  def initialize
@@ -194,10 +194,10 @@ module Bolt
194
194
  use_sudo = escalate && @target.options['run-as-command'].nil?
195
195
 
196
196
  command_str = inject_interpreter(options[:interpreter], command)
197
-
198
197
  if escalate
199
198
  if use_sudo
200
- sudo_flags = ["sudo", "-S", "-u", run_as, "-p", Sudoable.sudo_prompt]
199
+ sudo_exec = target.options['sudo-executable'] || "sudo"
200
+ sudo_flags = [sudo_exec, "-S", "-u", run_as, "-p", Sudoable.sudo_prompt]
201
201
  sudo_flags += ["-E"] if options[:environment]
202
202
  sudo_str = Shellwords.shelljoin(sudo_flags)
203
203
  else
@@ -7,6 +7,7 @@ module Bolt
7
7
  class Sudoable < Base
8
8
  class Connection
9
9
  attr_accessor :target
10
+
10
11
  def initialize(target)
11
12
  @target = target
12
13
  @run_as = nil
@@ -38,7 +39,8 @@ module Bolt
38
39
 
39
40
  def make_tempdir
40
41
  tmpdir = @target.options.fetch('tmpdir', '/tmp')
41
- tmppath = "#{tmpdir}/#{SecureRandom.uuid}"
42
+ script_dir = @target.options.fetch('script-dir', SecureRandom.uuid)
43
+ tmppath = File.join(tmpdir, script_dir)
42
44
  command = ['mkdir', '-m', 700, tmppath]
43
45
 
44
46
  result = execute(command)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Bolt
4
- VERSION = '1.42.0'
4
+ VERSION = '1.43.0'
5
5
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Aggregates the key/value pairs in the results of a ResultSet into a hash
4
- # mapping the keys to a hash of each distinct value and how many nodes returned
4
+ # mapping the keys to a hash of each distinct value and how many targets returned
5
5
  # that value for the key.
6
6
  Puppet::Functions.create_function(:'aggregate::count') do
7
7
  dispatch :aggregate_count do
@@ -19,3 +19,4 @@ Puppet::Functions.create_function(:'aggregate::nodes') do
19
19
  end
20
20
  end
21
21
  end
22
+
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Aggregates the key/value pairs in the results of a ResultSet into a hash
4
+ # mapping the keys to a hash of each distinct value and the list of targets
5
+ # returning that value for the key.
6
+ Puppet::Functions.create_function(:'aggregate::targets') do
7
+ dispatch :aggregate_targets do
8
+ param 'ResultSet', :resultset
9
+ end
10
+
11
+ def aggregate_targets(resultset)
12
+ resultset.each_with_object({}) do |result, agg|
13
+ result.value.each do |key, val|
14
+ agg[key] ||= {}
15
+ agg[key][val.to_s] ||= []
16
+ agg[key][val.to_s] << result.target.name
17
+ end
18
+ agg
19
+ end
20
+ end
21
+ end
@@ -2,7 +2,7 @@ plan aggregate::count(
2
2
  Optional[String[0]] $task = undef,
3
3
  Optional[String[0]] $command = undef,
4
4
  Optional[String[0]] $script = undef,
5
- TargetSpec $nodes,
5
+ TargetSpec $targets,
6
6
  Hash[String, Data] $params = {}
7
7
  ) {
8
8
 
@@ -24,11 +24,11 @@ plan aggregate::count(
24
24
  }
25
25
 
26
26
  $res = if ($task) {
27
- run_task($task, $nodes, $params)
27
+ run_task($task, $targets, $params)
28
28
  } elsif ($command) {
29
- run_command($command, $nodes, $params)
29
+ run_command($command, $targets, $params)
30
30
  } elsif ($script) {
31
- run_script($script, $nodes, $params)
31
+ run_script($script, $targets, $params)
32
32
  }
33
33
 
34
34
  return aggregate::count($res)
@@ -33,3 +33,4 @@ plan aggregate::nodes(
33
33
 
34
34
  return aggregate::nodes($res)
35
35
  }
36
+
@@ -0,0 +1,35 @@
1
+ plan aggregate::targets(
2
+ Optional[String[0]] $task = undef,
3
+ Optional[String[0]] $command = undef,
4
+ Optional[String[0]] $script = undef,
5
+ TargetSpec $targets,
6
+ Hash[String, Data] $params = {}
7
+ ) {
8
+
9
+ # Validation
10
+ $type_count = [$task, $command, $script].reduce(0) |$acc, $v| {
11
+ if ($v) {
12
+ $acc + 1
13
+ } else {
14
+ $acc
15
+ }
16
+ }
17
+
18
+ if ($type_count == 0) {
19
+ fail_plan("Must specify a command, script, or task to run", 'aggregate/invalid-params')
20
+ }
21
+
22
+ if ($type_count > 1) {
23
+ fail_plan("Must specify only one command, script, or task to run", 'aggregate/invalid-params')
24
+ }
25
+
26
+ $res = if ($task) {
27
+ run_task($task, $targets, $params)
28
+ } elsif ($command) {
29
+ run_command($command, $targets, $params)
30
+ } elsif ($script) {
31
+ run_script($script, $targets, $params)
32
+ }
33
+
34
+ return aggregate::targets($res)
35
+ }
@@ -1,22 +1,22 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Returns a ResultSet with canary/skipped-node errors for each Target provided.
3
+ # Returns a ResultSet with canary/skipped-target errors for each Target provided.
4
4
  #
5
5
  # This function takes a single parameter:
6
- # * List of nodes (Array[Variant[Target,String]])
6
+ # * List of targets (Array[Variant[Target,String]])
7
7
  #
8
8
  # Returns a ResultSet.
9
9
  Puppet::Functions.create_function(:'canary::skip') do
10
10
  dispatch :skip_result do
11
- param 'Array[Variant[Target,String]]', :nodes
11
+ param 'Array[Variant[Target,String]]', :targets
12
12
  end
13
13
 
14
- def skip_result(nodes)
15
- results = nodes.map do |node|
16
- node = Bolt::Target.new(node) unless node.is_a? Bolt::Target
17
- Bolt::Result.new(node, value: { '_error' => {
18
- 'msg' => "Skipped #{node.name} because of a previous failure",
19
- 'kind' => 'canary/skipped-node',
14
+ def skip_result(targets)
15
+ results = targets.map do |target|
16
+ target = Bolt::Target.new(target) unless target.is_a? Bolt::Target
17
+ Bolt::Result.new(target, value: { '_error' => {
18
+ 'msg' => "Skipped #{target.name} because of a previous failure",
19
+ 'kind' => 'canary/skipped-target',
20
20
  'details' => {}
21
21
  } })
22
22
  end
@@ -1,11 +1,11 @@
1
1
  # @summary
2
- # Run a task, command or script on canary nodes before running it on all nodes.
2
+ # Run a task, command or script on canary targets before running it on all targets.
3
3
  #
4
- # This plan accepts a action and a $nodes parameter. The action can be the name
4
+ # This plan accepts a action and a $targets parameter. The action can be the name
5
5
  # of a task, a script or a command to run. It will run the action on a canary
6
- # group of nodes and only continue to the rest of the nodes if it succeeds on
7
- # all canaries. This returns a ResultSet object with a Result for every node.
8
- # Any skipped nodes will have a 'canary/skipped-node' error kind.
6
+ # group of targets and only continue to the rest of the targets if it succeeds on
7
+ # all canaries. This returns a ResultSet object with a Result for every target.
8
+ # Any skipped targets will have a 'canary/skipped-target' error kind.
9
9
  #
10
10
  # @param task
11
11
  # The name of the task to run. Mutually exclusive with command and script.
@@ -13,7 +13,7 @@
13
13
  # The command to run. Mutually exclusive with task and script.
14
14
  # @param script
15
15
  # The script to run. Mutually exclusive with task and command.
16
- # @param nodes
16
+ # @param targets
17
17
  # The target to run on.
18
18
  # @param params
19
19
  # The parameters to use for the task.
@@ -23,13 +23,13 @@
23
23
  # @return ResultSet a merged resultset from running the action on all targets
24
24
  #
25
25
  # @example Run a command
26
- # run_plan(canary, command => 'whoami', nodes => $mynodes)
26
+ # run_plan(canary, command => 'whoami', targets => $mytargets)
27
27
  #
28
28
  plan canary(
29
29
  Optional[String[0]] $task = undef,
30
30
  Optional[String[0]] $command = undef,
31
31
  Optional[String[0]] $script = undef,
32
- TargetSpec $nodes,
32
+ TargetSpec $targets,
33
33
  Hash[String, Data] $params = {},
34
34
  Integer $canary_size = 1
35
35
  ) {
@@ -51,7 +51,7 @@ plan canary(
51
51
  fail_plan("Must specify only one command, script, or task to run", 'canary/invalid-params')
52
52
  }
53
53
 
54
- [$canaries, $rest] = canary::random_split(get_targets($nodes), $canary_size)
54
+ [$canaries, $rest] = canary::random_split(get_targets($targets), $canary_size)
55
55
  $catch_params = $params + { '_catch_errors' => true }
56
56
 
57
57
  if ($task) {
@@ -1,8 +1,8 @@
1
- plan puppetdb_fact(TargetSpec $nodes) {
2
- $targets = get_targets($nodes)
3
- $certnames = $targets.map |$target| { $target.host }
1
+ plan puppetdb_fact(TargetSpec $targets) {
2
+ $targs = get_targets($targets)
3
+ $certnames = $targs.map |$target| { $target.host }
4
4
  $pdb_facts = puppetdb_fact($certnames)
5
- $targets.each |$target| {
5
+ $targs.each |$target| {
6
6
  add_facts($target, $pdb_facts[$target.host])
7
7
  }
8
8
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bolt
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.42.0
4
+ version: 1.43.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Puppet
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2019-12-09 00:00:00.000000000 Z
11
+ date: 2019-12-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: addressable
@@ -377,7 +377,9 @@ files:
377
377
  - lib/bolt.rb
378
378
  - lib/bolt/analytics.rb
379
379
  - lib/bolt/applicator.rb
380
+ - lib/bolt/apply_inventory.rb
380
381
  - lib/bolt/apply_result.rb
382
+ - lib/bolt/apply_target.rb
381
383
  - lib/bolt/bolt_option_parser.rb
382
384
  - lib/bolt/boltdir.rb
383
385
  - lib/bolt/catalog.rb
@@ -495,8 +497,10 @@ files:
495
497
  - libexec/query_resources.rb
496
498
  - modules/aggregate/lib/puppet/functions/aggregate/count.rb
497
499
  - modules/aggregate/lib/puppet/functions/aggregate/nodes.rb
500
+ - modules/aggregate/lib/puppet/functions/aggregate/targets.rb
498
501
  - modules/aggregate/plans/count.pp
499
502
  - modules/aggregate/plans/nodes.pp
503
+ - modules/aggregate/plans/targets.pp
500
504
  - modules/canary/lib/puppet/functions/canary/merge.rb
501
505
  - modules/canary/lib/puppet/functions/canary/random_split.rb
502
506
  - modules/canary/lib/puppet/functions/canary/skip.rb
@@ -521,7 +525,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
521
525
  - !ruby/object:Gem::Version
522
526
  version: '0'
523
527
  requirements: []
524
- rubygems_version: 3.0.4
528
+ rubygems_version: 3.0.6
525
529
  signing_key:
526
530
  specification_version: 4
527
531
  summary: Execute commands remotely over SSH and WinRM