bolt 0.21.8 → 0.22.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.

checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 22fb0de3f6560ee5c6e57d1f0344e9994d93ae5a638531afde4586b619f137de
4
- data.tar.gz: c2e8780f18783e570e1d9cd44f52f722cce1329fcdfb3c32c3028b22d718f81e
3
+ metadata.gz: fe765b38afea737ae2308098b790fde05b73da5d4eaa661c2121313ccb675d48
4
+ data.tar.gz: 3280f5388127bce11a71f3aed0db819e7869f0d6751dedb3d0ec815c83623674
5
5
  SHA512:
6
- metadata.gz: e007695b192c794a2d391d9b2e2dbb2a732374e2aa5b7639ad31e0db7a98dfd5f64b08710dfb958ec7af0e56adb2628f7ef40380f9ad951f505231c303467441
7
- data.tar.gz: 78a3c221d8c45bc584b477071e47c9a402d0a99867db696eae6201edc739367b2f49a9ad7c465b9292ae4dbcaa1bd692df6dab878b90183e0e2e276af9d7b739
6
+ metadata.gz: 50c22855422b4a2cba254cb43647c8ead003b53816b082173a4383f143cf44e3b6a9ca20ff1792d986597e7256fffe875a7130fe036689860f8931f7c5622b29
7
+ data.tar.gz: 101c06ea0ad6836d3c773ab877fbc74f972f38c14a33d92f00d0da52e83cd797c58a1b502fc79c0b8edd85df3cccc9bfbbb972ce54d64d09de4a6d7ca1a1f31f
@@ -1,93 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'bolt/error'
4
-
5
- # Uploads the given file or directory to the given set of targets and returns the result from each upload.
6
- # This function does nothing if the list of targets is empty.
3
+ # This function wraps the upload_file function with a deprecation warning, for backward compatibility.
7
4
  Puppet::Functions.create_function(:file_upload, Puppet::Functions::InternalFunction) do
8
- # Upload a file.
9
- # @param source A source path, either an absolute path or a modulename/filename selector for a file in
10
- # <moduleroot>/files.
11
- # @param destination An absolute path on the target(s).
12
- # @param targets A pattern identifying zero or more targets. See {get_targets} for accepted patterns.
13
- # @param options Additional options: '_catch_errors', '_run_as'.
14
- # @return A list of results, one entry per target.
15
- # @example Upload a local file to Linux targets and change owner to 'root'
16
- # file_upload('/var/tmp/payload.tgz', '/tmp/payload.tgz', $targets, '_run_as' => 'root')
17
- # @example Upload a module file to a Windows target
18
- # file_upload('postgres/default.conf', 'C:/ProgramData/postgres/default.conf', $target)
19
- dispatch :file_upload do
20
- scope_param
21
- param 'String[1]', :source
22
- param 'String[1]', :destination
23
- param 'Boltlib::TargetSpec', :targets
24
- optional_param 'Hash[String[1], Any]', :options
25
- return_type 'ResultSet'
26
- end
27
-
28
- # Upload a file, logging the provided description.
29
- # @param source A source path, either an absolute path or a modulename/filename selector for a file in
30
- # <moduleroot>/files.
31
- # @param destination An absolute path on the target(s).
32
- # @param targets A pattern identifying zero or more targets. See {get_targets} for accepted patterns.
33
- # @param description A description to be output when calling this function.
34
- # @param options Additional options: '_catch_errors', '_run_as'.
35
- # @return A list of results, one entry per target.
36
- # @example Upload a file
37
- # file_upload('/var/tmp/payload.tgz', '/tmp/payload.tgz', $targets, 'Uploading payload to unpack')
38
- dispatch :file_upload_with_description do
39
- scope_param
40
- param 'String[1]', :source
41
- param 'String[1]', :destination
42
- param 'Boltlib::TargetSpec', :targets
43
- param 'String', :description
44
- optional_param 'Hash[String[1], Any]', :options
45
- return_type 'ResultSet'
46
- end
47
-
48
- def file_upload(scope, source, destination, targets, options = nil)
49
- file_upload_with_description(scope, source, destination, targets, nil, options)
50
- end
51
-
52
- def file_upload_with_description(scope, source, destination, targets, description = nil, options = nil)
53
- options ||= {}
54
- options = options.merge('_description' => description) if description
55
-
56
- unless Puppet[:tasks]
57
- raise Puppet::ParseErrorWithIssue.from_issue_and_stack(
58
- Puppet::Pops::Issues::TASK_OPERATION_NOT_SUPPORTED_WHEN_COMPILING, operation: 'file_upload'
59
- )
60
- end
61
-
5
+ def file_upload(*args)
62
6
  executor = Puppet.lookup(:bolt_executor) { nil }
63
- inventory = Puppet.lookup(:bolt_inventory) { nil }
64
- unless executor && inventory && Puppet.features.bolt?
65
- raise Puppet::ParseErrorWithIssue.from_issue_and_stack(
66
- Puppet::Pops::Issues::TASK_MISSING_BOLT, action: _('do file uploads')
67
- )
68
- end
69
-
70
- executor.report_function_call('file_upload')
7
+ executor&.report_function_call('file_upload')
71
8
 
72
- found = Puppet::Parser::Files.find_file(source, scope.compiler.environment)
73
- unless found && Puppet::FileSystem.exist?(found)
74
- raise Puppet::ParseErrorWithIssue.from_issue_and_stack(
75
- Puppet::Pops::Issues::NO_SUCH_FILE_OR_DIRECTORY, file: source
76
- )
77
- end
9
+ file, line = Puppet::Pops::PuppetStack.top_of_stack
78
10
 
79
- # Ensure that that given targets are all Target instances
80
- targets = inventory.get_targets(targets)
81
- if targets.empty?
82
- call_function('debug', "Simulating file upload of '#{found}' - no targets given - no action taken")
83
- r = Bolt::ResultSet.new([])
84
- else
85
- r = executor.file_upload(targets, found, destination, options)
86
- end
11
+ msg = "The file_upload function is deprecated and will be removed; use upload_file instead"
12
+ Puppet.puppet_deprecation_warning(msg, key: 'bolt-function/file_upload', file: file, line: line)
87
13
 
88
- if !r.ok && !options['_catch_errors']
89
- raise Bolt::RunFailure.new(r, 'upload_file', source)
90
- end
91
- r
14
+ call_function('upload_file', *args)
92
15
  end
93
16
  end
@@ -13,7 +13,7 @@ Puppet::Functions.create_function(:run_script, Puppet::Functions::InternalFuncti
13
13
  # @example Run a local script on Linux targets as 'root'
14
14
  # run_script('/var/tmp/myscript', $targets, '_run_as' => 'root')
15
15
  # @example Run a module-provided script with arguments
16
- # file_upload('iis/setup.ps1', $target, 'arguments' => ['/u', 'Administrator'])
16
+ # run_script('iis/setup.ps1', $target, 'arguments' => ['/u', 'Administrator'])
17
17
  dispatch :run_script do
18
18
  scope_param
19
19
  param 'String[1]', :script
@@ -31,7 +31,7 @@ Puppet::Functions.create_function(:run_script, Puppet::Functions::InternalFuncti
31
31
  # Additional options: '_catch_errors', '_run_as'.
32
32
  # @return A list of results, one entry per target.
33
33
  # @example Run a script
34
- # file_upload('/var/tmp/myscript', $targets, 'Downloading my application')
34
+ # run_script('/var/tmp/myscript', $targets, 'Downloading my application')
35
35
  dispatch :run_script_with_description do
36
36
  scope_param
37
37
  param 'String[1]', :script
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'bolt/error'
4
+ require 'bolt/pal'
4
5
 
5
6
  # Runs a given instance of a `Task` on the given set of targets and returns the result from each.
6
7
  # This function does nothing if the list of targets is empty.
@@ -120,7 +121,24 @@ Puppet::Functions.create_function(:run_task) do
120
121
  end
121
122
 
122
123
  unless Puppet::Pops::Types::TypeFactory.data.instance?(use_args)
123
- raise with_stack(:TYPE_NOT_DATA, 'Task parameters is not of type Data')
124
+ # generate a helpful error message about the type-mismatch between the type Data
125
+ # and the actual type of use_args
126
+ use_args_t = Puppet::Pops::Types::TypeCalculator.infer_set(use_args)
127
+ desc = Puppet::Pops::Types::TypeMismatchDescriber.singleton.describe_mismatch(
128
+ 'Task parameters are not of type Data. run_task()',
129
+ Puppet::Pops::Types::TypeFactory.data, use_args_t
130
+ )
131
+ raise with_stack(:TYPE_NOT_DATA, desc)
132
+ end
133
+
134
+ # Wrap parameters marked with '"sensitive": true' in the task metadata with a
135
+ # Sensitive wrapper type. This way it's not shown in logs.
136
+ if task.parameters
137
+ use_args.each do |k, v|
138
+ if task.parameters[k] && task.parameters[k]['sensitive']
139
+ use_args[k] = Puppet::Pops::Types::PSensitiveType::Sensitive.new(v)
140
+ end
141
+ end
124
142
  end
125
143
 
126
144
  if executor.noop
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bolt/error'
4
+
5
+ # Uploads the given file or directory to the given set of targets and returns the result from each upload.
6
+ # This function does nothing if the list of targets is empty.
7
+ Puppet::Functions.create_function(:upload_file, Puppet::Functions::InternalFunction) do
8
+ # Upload a file.
9
+ # @param source A source path, either an absolute path or a modulename/filename selector for a file in
10
+ # <moduleroot>/files.
11
+ # @param destination An absolute path on the target(s).
12
+ # @param targets A pattern identifying zero or more targets. See {get_targets} for accepted patterns.
13
+ # @param options Additional options: '_catch_errors', '_run_as'.
14
+ # @return A list of results, one entry per target.
15
+ # @example Upload a local file to Linux targets and change owner to 'root'
16
+ # upload_file('/var/tmp/payload.tgz', '/tmp/payload.tgz', $targets, '_run_as' => 'root')
17
+ # @example Upload a module file to a Windows target
18
+ # upload_file('postgres/default.conf', 'C:/ProgramData/postgres/default.conf', $target)
19
+ dispatch :upload_file do
20
+ scope_param
21
+ param 'String[1]', :source
22
+ param 'String[1]', :destination
23
+ param 'Boltlib::TargetSpec', :targets
24
+ optional_param 'Hash[String[1], Any]', :options
25
+ return_type 'ResultSet'
26
+ end
27
+
28
+ # Upload a file, logging the provided description.
29
+ # @param source A source path, either an absolute path or a modulename/filename selector for a file in
30
+ # <moduleroot>/files.
31
+ # @param destination An absolute path on the target(s).
32
+ # @param targets A pattern identifying zero or more targets. See {get_targets} for accepted patterns.
33
+ # @param description A description to be output when calling this function.
34
+ # @param options Additional options: '_catch_errors', '_run_as'.
35
+ # @return A list of results, one entry per target.
36
+ # @example Upload a file
37
+ # upload_file('/var/tmp/payload.tgz', '/tmp/payload.tgz', $targets, 'Uploading payload to unpack')
38
+ dispatch :upload_file_with_description do
39
+ scope_param
40
+ param 'String[1]', :source
41
+ param 'String[1]', :destination
42
+ param 'Boltlib::TargetSpec', :targets
43
+ param 'String', :description
44
+ optional_param 'Hash[String[1], Any]', :options
45
+ return_type 'ResultSet'
46
+ end
47
+
48
+ def upload_file(scope, source, destination, targets, options = nil)
49
+ upload_file_with_description(scope, source, destination, targets, nil, options)
50
+ end
51
+
52
+ def upload_file_with_description(scope, source, destination, targets, description = nil, options = nil)
53
+ options ||= {}
54
+ options = options.merge('_description' => description) if description
55
+
56
+ unless Puppet[:tasks]
57
+ raise Puppet::ParseErrorWithIssue.from_issue_and_stack(
58
+ Puppet::Pops::Issues::TASK_OPERATION_NOT_SUPPORTED_WHEN_COMPILING, operation: 'upload_file'
59
+ )
60
+ end
61
+
62
+ executor = Puppet.lookup(:bolt_executor) { nil }
63
+ inventory = Puppet.lookup(:bolt_inventory) { nil }
64
+ unless executor && inventory && Puppet.features.bolt?
65
+ raise Puppet::ParseErrorWithIssue.from_issue_and_stack(
66
+ Puppet::Pops::Issues::TASK_MISSING_BOLT, action: _('do file uploads')
67
+ )
68
+ end
69
+
70
+ executor.report_function_call('upload_file')
71
+
72
+ found = Puppet::Parser::Files.find_file(source, scope.compiler.environment)
73
+ unless found && Puppet::FileSystem.exist?(found)
74
+ raise Puppet::ParseErrorWithIssue.from_issue_and_stack(
75
+ Puppet::Pops::Issues::NO_SUCH_FILE_OR_DIRECTORY, file: source
76
+ )
77
+ end
78
+
79
+ # Ensure that that given targets are all Target instances
80
+ targets = inventory.get_targets(targets)
81
+ if targets.empty?
82
+ call_function('debug', "Simulating file upload of '#{found}' - no targets given - no action taken")
83
+ r = Bolt::ResultSet.new([])
84
+ else
85
+ r = executor.upload_file(targets, found, destination, options)
86
+ end
87
+
88
+ if !r.ok && !options['_catch_errors']
89
+ raise Bolt::RunFailure.new(r, 'upload_file', source)
90
+ end
91
+ r
92
+ end
93
+ end
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'puma/cli'
5
+ cli = Puma::CLI.new ARGV
6
+ cli.run
@@ -7,11 +7,10 @@ require 'json'
7
7
  require 'logging'
8
8
  require 'minitar'
9
9
  require 'open3'
10
+ require 'bolt/task'
10
11
  require 'bolt/util/puppet_log_level'
11
12
 
12
13
  module Bolt
13
- Task = Struct.new(:name, :implementations, :input_method)
14
-
15
14
  class Applicator
16
15
  def initialize(inventory, executor, modulepath, pdb_client, hiera_config, max_compiles)
17
16
  @inventory = inventory
@@ -41,7 +40,7 @@ module Bolt
41
40
  @custom_facts_task ||= begin
42
41
  path = File.join(libexec, 'custom_facts.rb')
43
42
  impl = { 'name' => 'custom_facts.rb', 'path' => path, 'requirements' => [], 'supports_noop' => true }
44
- Task.new('custom_facts', [impl], 'stdin')
43
+ Task.new(name: 'custom_facts', implementations: [impl], input_method: 'stdin')
45
44
  end
46
45
  end
47
46
 
@@ -49,7 +48,7 @@ module Bolt
49
48
  @catalog_apply_task ||= begin
50
49
  path = File.join(libexec, 'apply_catalog.rb')
51
50
  impl = { 'name' => 'apply_catalog.rb', 'path' => path, 'requirements' => [], 'supports_noop' => true }
52
- Task.new('apply_catalog', [impl], 'stdin')
51
+ Task.new(name: 'apply_catalog', implementations: [impl], input_method: 'stdin')
53
52
  end
54
53
  end
55
54
 
@@ -121,7 +120,9 @@ module Bolt
121
120
  'msg' => "Puppet is not installed on the target, please install it to enable 'apply'",
122
121
  'kind' => 'bolt/apply-error'
123
122
  })
124
- elsif exit_code == 1 && error_hash['msg'] =~ /Could not find executable 'ruby.exe'/
123
+ elsif exit_code == 1 &&
124
+ (error_hash['msg'] =~ /Could not find executable 'ruby.exe'/ ||
125
+ error_hash['msg'] =~ /The term 'ruby.exe' is not recognized as the name of a cmdlet/)
125
126
  # Windows does not have Ruby present
126
127
  Result.new(result.target, error:
127
128
  {
@@ -306,7 +306,7 @@ Available options are:
306
306
  end
307
307
 
308
308
  def read_arg_file(file)
309
- File.read(file)
309
+ File.read(File.expand_path(file))
310
310
  rescue StandardError => err
311
311
  raise Bolt::FileError.new("Error attempting to read #{file}: #{err}", file)
312
312
  end
@@ -296,7 +296,7 @@ module Bolt
296
296
  raise Bolt::CLIError, "A destination path must be specified"
297
297
  end
298
298
  validate_file('source file', src)
299
- executor.file_upload(targets, src, dest, executor_opts) do |event|
299
+ executor.upload_file(targets, src, dest, executor_opts) do |event|
300
300
  outputter.print_event(event)
301
301
  end
302
302
  end
@@ -18,14 +18,18 @@ module Bolt
18
18
  attr_reader :noop, :transports
19
19
  attr_accessor :run_as
20
20
 
21
+ # FIXME: There must be a better way
22
+ # https://makandracards.com/makandra/36011-ruby-do-not-mix-optional-and-keyword-arguments
21
23
  def initialize(concurrency = 1,
22
24
  analytics = Bolt::Analytics::NoopClient.new,
23
25
  noop = nil,
24
- bundled_content: nil)
26
+ bundled_content: nil,
27
+ load_config: true)
25
28
  @analytics = analytics
26
29
  @bundled_content = bundled_content
27
30
  @logger = Logging.logger[self]
28
31
  @plan_logging = false
32
+ @load_config = load_config
29
33
 
30
34
  @transports = Bolt::TRANSPORTS.each_with_object({}) do |(key, val), coll|
31
35
  coll[key.to_s] = Concurrent::Delay.new do
@@ -212,6 +216,7 @@ module Bolt
212
216
  log_action(description, targets) do
213
217
  notify = proc { |event| @notifier.notify(callback, event) if callback }
214
218
  options = { '_run_as' => run_as }.merge(options) if run_as
219
+ options = options.merge('_load_config' => @load_config)
215
220
  arguments['_task'] = task.name
216
221
 
217
222
  results = batch_execute(targets) do |transport, batch|
@@ -225,7 +230,7 @@ module Bolt
225
230
  end
226
231
  end
227
232
 
228
- def file_upload(targets, source, destination, options = {}, &callback)
233
+ def upload_file(targets, source, destination, options = {}, &callback)
229
234
  description = options.fetch('_description', "file upload from #{source} to #{destination}")
230
235
  log_action(description, targets) do
231
236
  notify = proc { |event| @notifier.notify(callback, event) if callback }
@@ -6,10 +6,18 @@ require 'bolt/util'
6
6
  module Bolt
7
7
  module PuppetDB
8
8
  class Config
9
- DEFAULT_TOKEN = File.expand_path('~/.puppetlabs/token')
10
- DEFAULT_CONFIG = { user: File.expand_path('~/.puppetlabs/client-tools/puppetdb.conf'),
11
- global: '/etc/puppetlabs/client-tools/puppetdb.conf',
12
- win_global: 'C:/ProgramData/PuppetLabs/client-tools/puppetdb.conf' }.freeze
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',
13
+ win_global: 'C:/ProgramData/PuppetLabs/client-tools/puppetdb.conf' }.freeze
14
+ else
15
+ DEFAULT_TOKEN = Bolt::Util.windows? ? 'nul' : '/dev/null'
16
+ DEFAULT_CONFIG = { user: '/etc/puppetlabs/puppet/puppetdb.conf',
17
+ global: '/etc/puppetlabs/puppet/puppetdb.conf',
18
+ win_global: 'C:/ProgramData/PuppetLabs/client-tools/puppetdb.conf' }.freeze
19
+
20
+ end
13
21
 
14
22
  def self.load_config(filename, options)
15
23
  global_path = Bolt::Util.windows? ? DEFAULT_CONFIG[:win_global] : DEFAULT_CONFIG[:global]
@@ -18,6 +18,22 @@ module Bolt
18
18
  @uri_obj = parse(uri)
19
19
  @options = options || {}
20
20
  @options.freeze
21
+
22
+ if @options['user']
23
+ @user = @options['user']
24
+ end
25
+
26
+ if @options['password']
27
+ @password = @options['password']
28
+ end
29
+
30
+ if @options['port']
31
+ @port = @options['port']
32
+ end
33
+
34
+ if @options['protocol']
35
+ @protocol = @options['protocol']
36
+ end
21
37
  end
22
38
 
23
39
  def update_conf(conf)
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bolt
4
+ Task = Struct.new(
5
+ :name,
6
+ :implementations,
7
+ :input_method,
8
+ :file,
9
+ :metadata
10
+ ) do
11
+
12
+ TASK_DEFAULTS = {
13
+ implementations: {},
14
+ input_method: 'both',
15
+ metadata: {}
16
+ }.freeze
17
+
18
+ def initialize(task)
19
+ super()
20
+ TASK_DEFAULTS.merge(task).each { |k, v| self[k] = v }
21
+ end
22
+ end
23
+ end
@@ -75,6 +75,15 @@ module Bolt
75
75
  end
76
76
  end
77
77
 
78
+ # Transform a parameter map to an environment variable map, with parameter names prefixed
79
+ # with 'PT_' and values transformed to JSON unless they're strings.
80
+ def envify_params(params)
81
+ params.each_with_object({}) do |(k, v), h|
82
+ v = v.to_json unless v.is_a?(String)
83
+ h["PT_#{k}"] = v
84
+ end
85
+ end
86
+
78
87
  # Raises an error if more than one target was given in the batch.
79
88
  #
80
89
  # The default implementations of batch_* strictly assume the transport is
@@ -152,6 +161,15 @@ module Bolt
152
161
  targets.map { |target| [target] }
153
162
  end
154
163
 
164
+ def from_api?(task)
165
+ if task.respond_to? :file
166
+ unless task.file.nil?
167
+ return true
168
+ end
169
+ end
170
+ false
171
+ end
172
+
155
173
  # Transports should override this method with their own implementation of running a command.
156
174
  def run_command(*_args)
157
175
  raise NotImplementedError, "run_command() must be implemented by the transport class"
@@ -171,6 +189,33 @@ module Bolt
171
189
  def upload(*_args)
172
190
  raise NotImplementedError, "upload() must be implemented by the transport class"
173
191
  end
192
+
193
+ # Unwraps any Sensitive data in an arguments Hash, so the plain-text is passed
194
+ # to the Task/Script.
195
+ #
196
+ # This works on deeply nested data structures composed of Hashes, Arrays, and
197
+ # and plain-old data types (int, string, etc).
198
+ def unwrap_sensitive_args(arguments)
199
+ # Skip this if Puppet isn't loaded
200
+ return arguments unless defined?(Puppet::Pops::Types::PSensitiveType::Sensitive)
201
+
202
+ case arguments
203
+ when Array
204
+ # iterate over the array, unwrapping all elements
205
+ arguments.map { |x| unwrap_sensitive_args(x) }
206
+ when Hash
207
+ # iterate over the arguments hash and unwrap all keys and values
208
+ arguments.each_with_object({}) { |(k, v), h|
209
+ h[unwrap_sensitive_args(k)] = unwrap_sensitive_args(v)
210
+ }
211
+ when Puppet::Pops::Types::PSensitiveType::Sensitive
212
+ # this value is Sensitive, unwrap it
213
+ unwrap_sensitive_args(arguments.unwrap)
214
+ else
215
+ # unknown data type, just return it
216
+ arguments
217
+ end
218
+ end
174
219
  end
175
220
  end
176
221
  end
@@ -70,6 +70,8 @@ module Bolt
70
70
  with_tmpscript(File.absolute_path(script), target.options['tmpdir']) do |file|
71
71
  logger.debug "Running '#{file}' with #{arguments}"
72
72
 
73
+ # unpack any Sensitive data AFTER we log
74
+ arguments = unwrap_sensitive_args(arguments)
73
75
  if arguments.empty?
74
76
  # We will always provide separated arguments, so work-around Open3's handling of a single
75
77
  # argument as the entire command string for script paths containing spaces.
@@ -84,11 +86,15 @@ module Bolt
84
86
  executable = target.select_impl(task, PROVIDED_FEATURES)
85
87
  raise "No suitable implementation of #{task.name} for #{target.name}" unless executable
86
88
 
89
+ # unpack any Sensitive data, write it to a separate variable because
90
+ # we log 'arguments' below
91
+ unwrapped_arguments = unwrap_sensitive_args(arguments)
87
92
  input_method = task.input_method || "both"
88
- stdin = STDIN_METHODS.include?(input_method) ? JSON.dump(arguments) : nil
89
- env = ENVIRONMENT_METHODS.include?(input_method) ? arguments : nil
93
+ stdin = STDIN_METHODS.include?(input_method) ? JSON.dump(unwrapped_arguments) : nil
94
+ env = ENVIRONMENT_METHODS.include?(input_method) ? envify_params(unwrapped_arguments) : nil
90
95
 
91
96
  with_tmpscript(executable, target.options['tmpdir']) do |script|
97
+ # log the arguments with sensitive data redacted, do NOT log unwrapped_arguments
92
98
  logger.debug("Running '#{script}' with #{arguments}")
93
99
 
94
100
  output = @conn.execute(script, stdin: stdin, env: env)
@@ -8,10 +8,7 @@ module Bolt
8
8
  class Local
9
9
  class Shell
10
10
  def execute(*command, options)
11
- if options[:env]
12
- env = options[:env].each_with_object({}) { |(k, v), h| h["PT_#{k}"] = v.to_s }
13
- command = [env] + command
14
- end
11
+ command = [options[:env]] + command if options[:env]
15
12
 
16
13
  if options[:stdin]
17
14
  stdout, stderr, rc = Open3.capture3(*command, stdin_data: options[:stdin])
@@ -11,7 +11,11 @@ require 'bolt/result'
11
11
  module Bolt
12
12
  module Transport
13
13
  class Orch < Base
14
- CONF_FILE = File.expand_path('~/.puppetlabs/client-tools/orchestrator.conf')
14
+ CONF_FILE = if !ENV['HOME'].nil?
15
+ File.expand_path('~/.puppetlabs/client-tools/orchestrator.conf')
16
+ else
17
+ '/etc/puppetlabs/client-tools/orchestrator.conf'
18
+ end
15
19
  BOLT_COMMAND_TASK = Struct.new(:name).new('bolt_shim::command').freeze
16
20
  BOLT_SCRIPT_TASK = Struct.new(:name).new('bolt_shim::script').freeze
17
21
  BOLT_UPLOAD_TASK = Struct.new(:name).new('bolt_shim::upload').freeze
@@ -149,6 +153,8 @@ module Bolt
149
153
  end
150
154
 
151
155
  begin
156
+ # unpack any Sensitive data
157
+ arguments = unwrap_sensitive_args(arguments)
152
158
  results = get_connection(targets.first.options).run_task(targets, task, arguments, options)
153
159
 
154
160
  process_run_results(targets, results)
@@ -63,8 +63,8 @@ module Bolt
63
63
  @transport_logger.level = :warn
64
64
  end
65
65
 
66
- def with_connection(target)
67
- conn = Connection.new(target, @transport_logger)
66
+ def with_connection(target, load_config = true)
67
+ conn = Connection.new(target, @transport_logger, load_config)
68
68
  conn.connect
69
69
  yield conn
70
70
  ensure
@@ -105,6 +105,9 @@ module Bolt
105
105
  end
106
106
 
107
107
  def run_script(target, script, arguments, options = {})
108
+ # unpack any Sensitive data
109
+ arguments = unwrap_sensitive_args(arguments)
110
+
108
111
  with_connection(target) do |conn|
109
112
  conn.running_as(options['_run_as']) do
110
113
  conn.with_remote_tempdir do |dir|
@@ -118,11 +121,10 @@ module Bolt
118
121
  end
119
122
 
120
123
  def run_task(target, task, arguments, options = {})
121
- executable = target.select_impl(task, PROVIDED_FEATURES)
122
- raise "No suitable implementation of #{task.name} for #{target.name}" unless executable
123
-
124
+ # unpack any Sensitive data
125
+ arguments = unwrap_sensitive_args(arguments)
124
126
  input_method = task.input_method || "both"
125
- with_connection(target) do |conn|
127
+ with_connection(target, options.fetch('_load_config', true)) do |conn|
126
128
  conn.running_as(options['_run_as']) do
127
129
  stdin, output = nil
128
130
 
@@ -134,15 +136,21 @@ module Bolt
134
136
  end
135
137
 
136
138
  if ENVIRONMENT_METHODS.include?(input_method)
137
- environment = arguments.inject({}) do |env, (param, val)|
138
- val = val.to_json unless val.is_a?(String)
139
- env.merge("PT_#{param}" => val)
140
- end
141
- execute_options[:environment] = environment
139
+ execute_options[:environment] = envify_params(arguments)
142
140
  end
143
141
 
144
142
  conn.with_remote_tempdir do |dir|
145
- remote_task_path = conn.write_remote_executable(dir, executable)
143
+ if from_api?(task)
144
+ filename = task.file['filename']
145
+ remote_task_path = conn.write_executable_from_content(dir,
146
+ Base64.decode64(task.file['file_content']),
147
+ filename)
148
+ else
149
+ executable = target.select_impl(task, PROVIDED_FEATURES)
150
+ raise "No suitable implementation of #{task.name} for #{target.name}" unless executable
151
+
152
+ remote_task_path = conn.write_remote_executable(dir, executable)
153
+ end
146
154
  if conn.run_as && stdin
147
155
  wrapper = make_wrapper_stringio(remote_task_path, stdin)
148
156
  remote_wrapper_path = conn.write_remote_executable(dir, wrapper, 'wrapper.sh')
@@ -54,10 +54,12 @@ module Bolt
54
54
  attr_reader :logger, :user, :target
55
55
  attr_writer :run_as
56
56
 
57
- def initialize(target, transport_logger)
57
+ def initialize(target, transport_logger, load_config = true)
58
58
  @target = target
59
+ @load_config = load_config
59
60
 
60
- @user = @target.user || Net::SSH::Config.for(target.host)[:user] || Etc.getlogin
61
+ ssh_user = load_config ? Net::SSH::Config.for(target.host)[:user] : nil
62
+ @user = @target.user || ssh_user || Etc.getlogin
61
63
  @run_as = nil
62
64
 
63
65
  @logger = Logging.logger[@target.host]
@@ -97,20 +99,26 @@ module Bolt
97
99
  end
98
100
  options[:timeout] = target.options['connect-timeout'] if target.options['connect-timeout']
99
101
 
100
- # Mirroring:
101
- # https://github.com/net-ssh/net-ssh/blob/master/lib/net/ssh/authentication/agent.rb#L80
102
- # https://github.com/net-ssh/net-ssh/blob/master/lib/net/ssh/authentication/pageant.rb#L403
103
- if defined?(UNIXSocket) && UNIXSocket
104
- if ENV['SSH_AUTH_SOCK'].to_s.empty?
105
- @logger.debug { "Disabling use_agent in net-ssh: ssh-agent is not available" }
106
- options[:use_agent] = false
107
- end
108
- elsif Bolt::Util.windows?
109
- pageant_wide = 'Pageant'.encode('UTF-16LE')
110
- if Win.FindWindow(pageant_wide, pageant_wide).to_i == 0
111
- @logger.debug { "Disabling use_agent in net-ssh: pageant process not running" }
112
- options[:use_agent] = false
102
+ if @load_config
103
+ # Mirroring:
104
+ # https://github.com/net-ssh/net-ssh/blob/master/lib/net/ssh/authentication/agent.rb#L80
105
+ # https://github.com/net-ssh/net-ssh/blob/master/lib/net/ssh/authentication/pageant.rb#L403
106
+ if defined?(UNIXSocket) && UNIXSocket
107
+ if ENV['SSH_AUTH_SOCK'].to_s.empty?
108
+ @logger.debug { "Disabling use_agent in net-ssh: ssh-agent is not available" }
109
+ options[:use_agent] = false
110
+ end
111
+ elsif Bolt::Util.windows?
112
+ pageant_wide = 'Pageant'.encode('UTF-16LE')
113
+ if Win.FindWindow(pageant_wide, pageant_wide).to_i == 0
114
+ @logger.debug { "Disabling use_agent in net-ssh: pageant process not running" }
115
+ options[:use_agent] = false
116
+ end
113
117
  end
118
+ else
119
+ # Disable ssh config and ssh-agent if requested via load_config
120
+ options[:config] = false
121
+ options[:use_agent] = false
114
122
  end
115
123
 
116
124
  @session = Net::SSH.start(target.host, @user, options)
@@ -302,12 +310,19 @@ module Bolt
302
310
 
303
311
  def write_remote_executable(dir, file, filename = nil)
304
312
  filename ||= File.basename(file)
305
- remote_path = "#{dir}/#{filename}"
313
+ remote_path = File.join(dir.to_s, filename)
306
314
  write_remote_file(file, remote_path)
307
315
  make_executable(remote_path)
308
316
  remote_path
309
317
  end
310
318
 
319
+ def write_executable_from_content(dest, content, filename)
320
+ remote_path = File.join(dest.to_s, filename)
321
+ @session.scp.upload!(StringIO.new(content), remote_path)
322
+ make_executable(remote_path)
323
+ remote_path
324
+ end
325
+
311
326
  def make_executable(path)
312
327
  result = execute(['chmod', 'u+x', path])
313
328
  if result.exit_code != 0
@@ -69,6 +69,9 @@ module Bolt
69
69
  end
70
70
 
71
71
  def run_script(target, script, arguments, _options = {})
72
+ # unpack any Sensitive data
73
+ arguments = unwrap_sensitive_args(arguments)
74
+
72
75
  with_connection(target) do |conn|
73
76
  conn.with_remote_file(script) do |remote_path|
74
77
  if powershell_file?(remote_path)
@@ -103,8 +106,21 @@ catch
103
106
  end
104
107
 
105
108
  def run_task(target, task, arguments, _options = {})
106
- executable = target.select_impl(task, PROVIDED_FEATURES)
107
- raise "No suitable implementation of #{task.name} for #{target.name}" unless executable
109
+ if from_api?(task)
110
+ # TODO: Remove as part of BOLT-664
111
+ dir = Dir.mktmpdir
112
+ executable = File.join(dir, task.file['filename'])
113
+ File.open(executable, 'w') { |f|
114
+ f.write(Base64.decode64(task.file['file_content']))
115
+ }
116
+ task.input_method = powershell_file?(executable) ? 'powershell' : 'both'
117
+ else
118
+ executable = target.select_impl(task, PROVIDED_FEATURES)
119
+ raise "No suitable implementation of #{task.name} for #{target.name}" unless executable
120
+ end
121
+
122
+ # unpack any Sensitive data
123
+ arguments = unwrap_sensitive_args(arguments)
108
124
 
109
125
  input_method = task.input_method
110
126
  input_method ||= powershell_file?(executable) ? 'powershell' : 'both'
@@ -114,12 +130,11 @@ catch
114
130
  end
115
131
 
116
132
  if ENVIRONMENT_METHODS.include?(input_method)
117
- arguments.each do |(arg, val)|
118
- val = val.to_json unless val.is_a?(String)
119
- cmd = "[Environment]::SetEnvironmentVariable('PT_#{arg}', @'\n#{val}\n'@)"
133
+ envify_params(arguments).each do |(arg, val)|
134
+ cmd = "[Environment]::SetEnvironmentVariable('#{arg}', @'\n#{val}\n'@)"
120
135
  result = conn.execute(cmd)
121
136
  if result.exit_code != 0
122
- raise EnvironmentVarError(var, value)
137
+ raise Bolt::Node::EnvironmentVarError.new(arg, val)
123
138
  end
124
139
  end
125
140
  end
@@ -147,6 +162,10 @@ try { & "#{remote_path}" @taskArgs } catch { Write-Error $_.Exception; exit 1 }
147
162
  path, args = *process_from_extension(remote_path)
148
163
  conn.execute_process(path, args, stdin)
149
164
  end
165
+
166
+ if from_api?(task)
167
+ FileUtils.remove_entry dir
168
+ end
150
169
  Bolt::Result.for_task(target, output.stdout.string,
151
170
  output.stderr.string,
152
171
  output.exit_code)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Bolt
4
- VERSION = '0.21.8'
4
+ VERSION = '0.22.0'
5
5
  end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'sinatra'
4
+ require 'bolt'
5
+ require 'bolt/task'
6
+ require 'json'
7
+
8
+ class TransportAPI < Sinatra::Base
9
+ # This disables Sinatra's error page generation
10
+ set :show_exceptions, false
11
+
12
+ get '/' do
13
+ 200
14
+ end
15
+
16
+ get '/500_error' do
17
+ raise 'Unexpected error'
18
+ end
19
+
20
+ post '/ssh/run_task' do
21
+ content_type :json
22
+
23
+ body = JSON.parse(request.body.read)
24
+ keys = %w[user password port ssh-key-content connect-timeout run-as-command run-as
25
+ tmpdir host-key-check known-hosts-content private-key-content sudo-password]
26
+ opts = body['target'].select { |k, _| keys.include? k }
27
+
28
+ if opts['private-key-content'] && opts['password']
29
+ return [400, "Only include one of 'password' and 'private-key-content'"]
30
+ end
31
+ if opts['private-key-content']
32
+ opts['private-key'] = { 'key-data' => opts['private-key-content'] }
33
+ opts.delete('private-key-content')
34
+ end
35
+
36
+ target = [Bolt::Target.new(body['target']['hostname'], opts)]
37
+ task = Bolt::Task.new(body['task'])
38
+ parameters = body['parameters'] || {}
39
+
40
+ executor = Bolt::Executor.new(load_config: false)
41
+
42
+ # Since this will only be on one node we can just return the first result
43
+ results = executor.run_task(target, task, parameters)
44
+ [200, results.first.to_json]
45
+ end
46
+
47
+ post '/winrm/run_task' do
48
+ content_type :json
49
+
50
+ body = JSON.parse(request.body.read)
51
+ keys = %w[user password port connect-timeout ssl ssl-verify tmpdir cacert extensions]
52
+ opts = body['target'].select { |k, _| keys.include? k }
53
+ opts['protocol'] = 'winrm'
54
+ target = [Bolt::Target.new(body['target']['hostname'], opts)]
55
+ task = Bolt::Task.new(body['task'])
56
+ parameters = body['parameters'] || {}
57
+
58
+ executor = Bolt::Executor.new(load_config: false)
59
+
60
+ # Since this will only be on one node we can just return the first result
61
+ results = executor.run_task(target, task, parameters)
62
+ [200, results.first.to_json]
63
+ end
64
+
65
+ error 404 do
66
+ [404, "Could not find route #{request.path}"]
67
+ end
68
+
69
+ error 500 do
70
+ e = env['sinatra.error']
71
+ [500, "500: Unknown error: #{e.message}"]
72
+ end
73
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/auth/rack'
4
+
5
+ class TransportACL < Rails::Auth::ErrorPage::Middleware
6
+ class X509Matcher
7
+ def initialize(options)
8
+ @options = options.freeze
9
+ end
10
+
11
+ def match(env)
12
+ certificate = Rails::Auth::X509::Certificate.new(env['puma.peercert'])
13
+ # This can be extended fairly easily to search OpenSSL::X509::Certificate#extensions for subjectAltNames.
14
+ @options.all? { |name, value| certificate[name] == value }
15
+ end
16
+ end
17
+
18
+ def initialize(app, whitelist)
19
+ acls = []
20
+ whitelist.each do |entry|
21
+ acls << {
22
+ 'resources' => [
23
+ {
24
+ 'method' => 'ALL',
25
+ 'path' => '/.*'
26
+ }
27
+ ],
28
+ 'allow_x509_subject' => {
29
+ 'cn' => entry
30
+ }
31
+ }
32
+ end
33
+ acl = Rails::Auth::ACL.new(acls, matchers: { allow_x509_subject: X509Matcher })
34
+ mid = Rails::Auth::ACL::Middleware.new(app, acl: acl)
35
+ super(mid, page_body: 'Access denied')
36
+ end
37
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'hocon'
4
+
5
+ class TransportConfig
6
+ attr_accessor :host, :port, :ssl_cert, :ssl_key, :ssl_ca_cert, :ssl_cipher_suites,
7
+ :loglevel, :logfile, :whitelist, :concurrency
8
+
9
+ def initialize(global = nil, local = nil)
10
+ @host = '127.0.0.1'
11
+ @port = 62658
12
+ @ssl_cert = nil
13
+ @ssl_key = nil
14
+ @ssl_ca_cert = nil
15
+ @ssl_cipher_suites = ['ECDHE-ECDSA-AES256-GCM-SHA384',
16
+ 'ECDHE-RSA-AES256-GCM-SHA384',
17
+ 'ECDHE-ECDSA-CHACHA20-POLY1305',
18
+ 'ECDHE-RSA-CHACHA20-POLY1305',
19
+ 'ECDHE-ECDSA-AES128-GCM-SHA256',
20
+ 'ECDHE-RSA-AES128-GCM-SHA256',
21
+ 'ECDHE-ECDSA-AES256-SHA384',
22
+ 'ECDHE-RSA-AES256-SHA384',
23
+ 'ECDHE-ECDSA-AES128-SHA256',
24
+ 'ECDHE-RSA-AES128-SHA256']
25
+
26
+ @loglevel = 'notice'
27
+ @logfile = nil
28
+ @whitelist = nil
29
+ @concurrency = 100
30
+
31
+ global_path = global || '/etc/puppetlabs/bolt-server/conf.d/bolt-server.conf'
32
+ local_path = local || File.join(ENV['HOME'].to_s, ".puppetlabs", "bolt-server.conf")
33
+
34
+ load_config(global_path)
35
+ load_config(local_path)
36
+ validate
37
+ end
38
+
39
+ def load_config(path)
40
+ begin
41
+ parsed_hocon = Hocon.load(path)['bolt-server']
42
+ rescue Hocon::ConfigError => e
43
+ raise "Hocon data in '#{path}' failed to load.\n Error: '#{e.message}'"
44
+ rescue Errno::EACCES
45
+ raise "Your user doesn't have permission to read #{path}"
46
+ end
47
+
48
+ unless parsed_hocon.nil?
49
+ %w[host port ssl-cert ssl-key ssl-ca-cert ssl-cipher-suites loglevel logfile whitelist concurrency].each do |key|
50
+ varname = '@' + key.tr('-', '_')
51
+ instance_variable_set(varname, parsed_hocon[key]) if parsed_hocon.key?(key)
52
+ end
53
+ end
54
+ end
55
+
56
+ def validate
57
+ required_keys = %w[ssl_cert ssl_key ssl_ca_cert]
58
+ ssl_keys = %w[ssl_cert ssl_key ssl_ca_cert]
59
+ required_keys.each do |k|
60
+ next unless send(k).nil?
61
+ raise Bolt::ValidationError, <<-MSG
62
+ You must configure #{k} in either /etc/puppetlabs/bolt-server/conf.d/bolt-server.conf or ~/.puppetlabs/bolt-server.conf
63
+ MSG
64
+ end
65
+
66
+ unless @port.is_a?(Integer) && @port > 0
67
+ raise Bolt::ValidationError, "Configured 'port' must be a valid integer greater than 0"
68
+ end
69
+ ssl_keys.each do |sk|
70
+ unless File.file?(send(sk)) && File.readable?(send(sk))
71
+ raise Bolt::ValidationError, "Configured #{sk} must be a valid filepath"
72
+ end
73
+ end
74
+
75
+ unless @ssl_cipher_suites.is_a?(Array)
76
+ raise Bolt::ValidationError, "Configured 'ssl-cipher-suites' must be an array of cipher suite names"
77
+ end
78
+
79
+ unless @whitelist.nil? || @whitelist.is_a?(Array)
80
+ raise Bolt::ValidationError, "Configured 'whitelist' must be an array of names"
81
+ end
82
+
83
+ unless @concurrency.is_a?(Integer) && @concurrency.positive?
84
+ raise Bolt::ValidationError, "Configured 'concurrency' must be a positive integer"
85
+ end
86
+ end
87
+ end
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: 0.21.8
4
+ version: 0.22.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Puppet
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2018-08-23 00:00:00.000000000 Z
11
+ date: 2018-09-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: addressable
@@ -282,6 +282,7 @@ email:
282
282
  executables:
283
283
  - bolt
284
284
  - bolt-inventory-pdb
285
+ - bolt-server
285
286
  extensions: []
286
287
  extra_rdoc_files: []
287
288
  files:
@@ -302,12 +303,14 @@ files:
302
303
  - bolt-modules/boltlib/lib/puppet/functions/run_task.rb
303
304
  - bolt-modules/boltlib/lib/puppet/functions/set_feature.rb
304
305
  - bolt-modules/boltlib/lib/puppet/functions/set_var.rb
306
+ - bolt-modules/boltlib/lib/puppet/functions/upload_file.rb
305
307
  - bolt-modules/boltlib/lib/puppet/functions/vars.rb
306
308
  - bolt-modules/boltlib/lib/puppet/functions/without_default_logging.rb
307
309
  - bolt-modules/boltlib/types/planresult.pp
308
310
  - bolt-modules/boltlib/types/targetspec.pp
309
311
  - exe/bolt
310
312
  - exe/bolt-inventory-pdb
313
+ - exe/bolt-server
311
314
  - lib/bolt.rb
312
315
  - lib/bolt/analytics.rb
313
316
  - lib/bolt/applicator.rb
@@ -340,6 +343,7 @@ files:
340
343
  - lib/bolt/result.rb
341
344
  - lib/bolt/result_set.rb
342
345
  - lib/bolt/target.rb
346
+ - lib/bolt/task.rb
343
347
  - lib/bolt/transport/base.rb
344
348
  - lib/bolt/transport/local.rb
345
349
  - lib/bolt/transport/local/shell.rb
@@ -353,6 +357,9 @@ files:
353
357
  - lib/bolt/util/puppet_log_level.rb
354
358
  - lib/bolt/version.rb
355
359
  - lib/bolt_ext/puppetdb_inventory.rb
360
+ - lib/bolt_ext/server.rb
361
+ - lib/bolt_ext/server_acl.rb
362
+ - lib/bolt_ext/server_config.rb
356
363
  - lib/bolt_spec/plans.rb
357
364
  - lib/bolt_spec/plans/mock_executor.rb
358
365
  - lib/bolt_spec/run.rb