bolt 1.30.1 → 1.31.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.

@@ -4,41 +4,27 @@ module Bolt
4
4
  class Plugin
5
5
  class Task
6
6
  def hooks
7
- %w[inventory_targets inventory_config]
7
+ %i[validate_resolve_reference puppet_library resolve_reference]
8
8
  end
9
9
 
10
10
  def name
11
11
  'task'
12
12
  end
13
13
 
14
- # This creates it's own PAL so we don't have to pass a promise around
15
- #
16
- def initialize(config)
17
- @config = config
18
- end
19
-
20
- attr_reader :config
14
+ attr_accessor :pal, :executor, :inventory
21
15
 
22
- def pal
23
- # Hiera config should not be used yet.
24
- @pal ||= Bolt::PAL.new(config.modulepath, config.hiera_config)
16
+ def initialize(context:, **_opts)
17
+ @context = context
25
18
  end
26
19
 
27
- def executor
28
- # Analytics should be handled at a higher level so create a new executor.
29
- @executor ||= Bolt::Executor.new
30
- end
20
+ def run_task(opts)
21
+ params = opts['parameters'] || {}
22
+ options = { '_catch_errors' => true }
31
23
 
32
- def inventory
33
- @inventory ||= Bolt::Inventory.new({}, config)
34
- end
24
+ raise Bolt::ValidationError, "Task plugin requires that the 'task' is specified" unless opts['task']
25
+ task = @context.get_validated_task(opts['task'], params)
35
26
 
36
- def run_task(opts)
37
- result = pal.run_task(opts['task'],
38
- 'localhost',
39
- opts['parameters'] || {},
40
- executor,
41
- inventory).first
27
+ result = @context.run_local_task(task, params, options).first
42
28
 
43
29
  raise Bolt::Error.new(result.error_hash['msg'], result.error_hash['kind']) if result.error_hash
44
30
  result
@@ -46,45 +32,18 @@ module Bolt
46
32
 
47
33
  def validate_options(opts)
48
34
  raise Bolt::ValidationError, "Task plugin requires that the 'task' is specified" unless opts['task']
49
-
50
- task = pal.task_signature(opts['task'])
51
-
52
- raise Bolt::ValidationError, "Could not find task #{opts['task']}" unless task
53
-
54
- errors = []
55
- unless task.runnable_with?(opts['parameters'] || {}) { |msg| errors << msg }
56
- # This relies on runnable with printing a partial message before the first real error
57
- raise Bolt::ValidationError, "Invalid parameters for #{errors.join("\n")}"
58
- end
35
+ @context.get_validated_task(opts['task'], opts['parameters'] || {})
59
36
  end
60
- alias validate_inventory_config validate_options
37
+ alias validate_resolve_reference validate_options
61
38
 
62
- def inventory_config(opts)
39
+ def resolve_reference(opts)
63
40
  result = run_task(opts)
64
41
 
65
- unless result.value.include?('config')
66
- raise Bolt::ValidationError, "Task result did not return 'config': #{result.value}"
67
- end
68
-
69
- result['config']
70
- end
71
-
72
- def inventory_targets(opts)
73
- raise Bolt::ValidationError, "Task plugin requires that the 'task' is specified" unless opts['task']
74
-
75
- result = run_task(opts)
76
-
77
- targets = result['targets']
78
- unless targets.is_a?(Array)
79
- raise Bolt::ValidationError, "Task result did not return a targets array: #{result.value}"
80
- end
81
-
82
- unless targets.all? { |t| t.is_a?(Hash) }
83
- msg = "All targets returned by an inventory targets task must be hashes, got: #{targets}"
84
- raise Bolt::ValidationError, msg
42
+ unless result.value.include?('value')
43
+ raise Bolt::ValidationError, "Task result did not return 'value': #{result.value}"
85
44
  end
86
45
 
87
- targets
46
+ result['value']
88
47
  end
89
48
 
90
49
  def puppet_library(opts, target, apply_prep)
@@ -9,7 +9,7 @@ module Bolt
9
9
  'config', 'backend']
10
10
  REQ_KEYS = Set['dir', 'resource_type']
11
11
 
12
- def initialize
12
+ def initialize(*_args)
13
13
  @logger = Logging.logger[self]
14
14
  end
15
15
 
@@ -18,7 +18,7 @@ module Bolt
18
18
  end
19
19
 
20
20
  def hooks
21
- ['inventory_targets']
21
+ [:resolve_reference]
22
22
  end
23
23
 
24
24
  def warn_missing_property(name, property)
@@ -41,7 +41,7 @@ module Bolt
41
41
  end
42
42
  end
43
43
 
44
- def inventory_targets(opts)
44
+ def resolve_reference(opts)
45
45
  validate_options(opts)
46
46
 
47
47
  state = load_statefile(opts)
@@ -55,16 +55,16 @@ module Bolt
55
55
  end
56
56
 
57
57
  def hooks
58
- ['inventory_config']
58
+ [:resolve_reference]
59
59
  end
60
60
 
61
- def initialize(config)
61
+ def initialize(config:, **_opts)
62
62
  validate_config(config)
63
63
  @config = config
64
64
  @logger = Logging.logger[self]
65
65
  end
66
66
 
67
- def inventory_config(opts)
67
+ def resolve_reference(opts)
68
68
  validate_options(opts)
69
69
 
70
70
  header = {
data/lib/bolt/secret.rb CHANGED
@@ -3,14 +3,15 @@
3
3
  module Bolt
4
4
  class Secret
5
5
  def self.execute(plugins, outputter, options)
6
+ plugin = options[:plugin] || 'pkcs7'
6
7
  case options[:action]
7
8
  when 'createkeys'
8
- plugins.get_hook('pkcs7', :secret_createkeys).call
9
+ plugins.get_hook(plugin, :secret_createkeys).call
9
10
  when 'encrypt'
10
- encrypted = plugins.get_hook('pkcs7', :secret_encrypt).call('plaintext-value' => options[:object])
11
+ encrypted = plugins.get_hook(plugin, :secret_encrypt).call('plaintext_value' => options[:object])
11
12
  outputter.print_message(encrypted)
12
13
  when 'decrypt'
13
- decrypted = plugins.get_hook('pkcs7', :secret_decrypt).call('encrypted-value' => options[:object])
14
+ decrypted = plugins.get_hook(plugin, :secret_decrypt).call('encrypted_value' => options[:object])
14
15
  outputter.print_message(decrypted)
15
16
  end
16
17
 
@@ -4,7 +4,7 @@ module Bolt
4
4
  class Secret
5
5
  class Base
6
6
  def hooks
7
- %w[inventory_config secret_encrypt secret_decrypt secret_createkeys]
7
+ %i[resolve_reference secret_encrypt secret_decrypt secret_createkeys validate_resolve_reference]
8
8
  end
9
9
 
10
10
  def encode(raw)
@@ -23,18 +23,22 @@ module Bolt
23
23
  end
24
24
 
25
25
  def secret_encrypt(opts)
26
- encrypted = encrypt_value(opts['plaintext-value'])
26
+ encrypted = encrypt_value(opts['plaintext_value'])
27
27
  encode(encrypted)
28
28
  end
29
29
 
30
30
  def secret_decrypt(opts)
31
- raw, _plugin = decode(opts['encrypted-value'])
31
+ raw, _plugin = decode(opts['encrypted_value'])
32
32
  decrypt_value(raw)
33
33
  end
34
- alias inventory_config secret_decrypt
35
-
36
- def validate_inventory_config(opts)
37
- decode(opts['encrypted-value'])
34
+ alias resolve_reference secret_decrypt
35
+
36
+ def validate_resolve_reference(opts)
37
+ # TODO: Remove deprecation warning
38
+ if opts.include?('encrypted-value')
39
+ raise Bolt::ValidationError, "The 'encrypted-value' key is deprecated migrate to to 'encrypted_value'"
40
+ end
41
+ decode(opts['encrypted_value'])
38
42
  end
39
43
  end
40
44
  end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bolt
4
+ class Task
5
+ module Run
6
+ module_function
7
+
8
+ # TODO: we should probably use a Bolt::Task for this
9
+ def validate_params(task_signature, params)
10
+ task_signature.runnable_with?(params) do |mismatch_message|
11
+ raise Bolt::ValidationError, mismatch_message
12
+ end || (raise Bolt::ValidationError, 'Task parameters do not match')
13
+
14
+ unless Puppet::Pops::Types::TypeFactory.data.instance?(params)
15
+ # generate a helpful error message about the type-mismatch between the type Data
16
+ # and the actual type of use_args
17
+ use_args_t = Puppet::Pops::Types::TypeCalculator.infer_set(params)
18
+ desc = Puppet::Pops::Types::TypeMismatchDescriber.singleton.describe_mismatch(
19
+ 'Task parameters are not of type Data. run_task()',
20
+ Puppet::Pops::Types::TypeFactory.data, use_args_t
21
+ )
22
+ raise Bolt::ValidationError, desc
23
+ end
24
+ nil
25
+ end
26
+
27
+ def wrap_sensitive(task, params)
28
+ if (spec = task.metadata['parameters'])
29
+ params.each_with_object({}) do |(param, val), wrapped|
30
+ wrapped[param] = if spec.dig(param, 'sensitive')
31
+ Puppet::Pops::Types::PSensitiveType::Sensitive.new(val)
32
+ else
33
+ val
34
+ end
35
+ end
36
+ else
37
+ params
38
+ end
39
+ end
40
+
41
+ def run_task(task, targets, params, options, executor)
42
+ if targets.empty?
43
+ Bolt::ResultSet.new([])
44
+ else
45
+ result = executor.run_task(targets, task, params, options)
46
+
47
+ if !result.ok && !options['_catch_errors']
48
+ raise Bolt::RunFailure.new(result, 'run_task', task.name)
49
+ end
50
+ result
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -24,8 +24,10 @@ module Bolt
24
24
  acc[k] = opts[k] if opts.include?(k)
25
25
  end
26
26
  client_opts['User-Agent'] = "Bolt/#{VERSION}"
27
+ %w[token-file cacert].each do |f|
28
+ client_opts[f] = File.expand_path(client_opts[f]) if client_opts[f]
29
+ end
27
30
  logger.debug("Creating orchestrator client for #{client_opts}")
28
-
29
31
  @client = OrchestratorClient.new(client_opts, true)
30
32
  @plan_job = start_plan(plan_context)
31
33
  logger.debug("Started plan #{@plan_job}")
@@ -42,6 +42,10 @@ module Bolt
42
42
  raise Bolt::ValidationError, 'SMB file transfers are not allowed with SSL enabled'
43
43
  end
44
44
 
45
+ if ssl_flag && (ca_path = options['cacert'])
46
+ Bolt::Util.validate_file('cacert', ca_path)
47
+ end
48
+
45
49
  ssl_verify_flag = options['ssl-verify']
46
50
  unless !!ssl_verify_flag == ssl_verify_flag
47
51
  raise Bolt::ValidationError, 'ssl-verify option must be a Boolean true or false'
@@ -41,13 +41,14 @@ module Bolt
41
41
 
42
42
  transport = :kerberos if target.options['realm']
43
43
  endpoint = "#{scheme}://#{target.host}:#{@port}/wsman"
44
+ cacert = target.options['cacert'] && target.options['ssl'] ? File.expand_path(target.options['cacert']) : nil
44
45
  options = { endpoint: endpoint,
45
46
  # https://github.com/WinRb/WinRM/issues/270
46
47
  user: target.options['realm'] ? 'dummy' : @user,
47
48
  password: target.options['realm'] ? 'dummy' : target.password,
48
49
  retry_limit: 1,
49
50
  transport: transport,
50
- ca_trust_path: target.options['cacert'],
51
+ ca_trust_path: cacert,
51
52
  realm: target.options['realm'],
52
53
  no_ssl_peer_verification: !target.options['ssl-verify'] }
53
54
 
data/lib/bolt/util.rb CHANGED
@@ -184,6 +184,12 @@ module Bolt
184
184
  File.stat(File.expand_path(path))
185
185
  end
186
186
 
187
+ def class_name_to_file_name(cls_name)
188
+ # Note this turns Bolt::CLI -> 'bolt/cli' not 'bolt/c_l_i'
189
+ # this won't handle Bolt::Inventory2Foo
190
+ cls_name.gsub(/([a-z])([A-Z])/, '\1_\2').gsub('::', '/').downcase
191
+ end
192
+
187
193
  def validate_file(type, path, allow_dir = false)
188
194
  stat = file_stat(path)
189
195
 
data/lib/bolt/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Bolt
4
- VERSION = '1.30.1'
4
+ VERSION = '1.31.0'
5
5
  end
@@ -27,17 +27,21 @@ module BoltServer
27
27
  executor: Concurrent::SingleThreadExecutor.new,
28
28
  purge_interval: PURGE_INTERVAL,
29
29
  purge_timeout: PURGE_TIMEOUT,
30
- purge_ttl: PURGE_TTL)
30
+ purge_ttl: PURGE_TTL,
31
+ cache_dir_mutex: Concurrent::ReadWriteLock.new,
32
+ do_purge: true)
31
33
  @executor = executor
32
34
  @cache_dir = config['cache-dir']
33
35
  @config = config
34
36
  @logger = Logging.logger[self]
35
- @cache_dir_mutex = Concurrent::ReadWriteLock.new
37
+ @cache_dir_mutex = cache_dir_mutex
36
38
 
37
- @purge = Concurrent::TimerTask.new(execution_interval: purge_interval,
38
- timeout_interval: purge_timeout,
39
- run_now: true) { expire(purge_ttl) }
40
- @purge.execute
39
+ if do_purge
40
+ @purge = Concurrent::TimerTask.new(execution_interval: purge_interval,
41
+ timeout_interval: purge_timeout,
42
+ run_now: true) { expire(purge_ttl) }
43
+ @purge.execute
44
+ end
41
45
  end
42
46
 
43
47
  def tmppath
@@ -0,0 +1,44 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-04/schema#",
3
+ "title": "run_script request",
4
+ "description": "POST <transport>/run_script request schema",
5
+ "type": "object",
6
+ "properties": {
7
+ "script": {
8
+ "type": "object",
9
+ "properties": {
10
+ "filename": {
11
+ "type": "string"
12
+ },
13
+ "uri": {
14
+ "type": "object",
15
+ "properties": {
16
+ "path": {
17
+ "type": "string"
18
+ },
19
+ "params": {
20
+ "type": "object"
21
+ }
22
+ },
23
+ "required": [
24
+ "path",
25
+ "params"
26
+ ]
27
+ },
28
+ "sha256": {
29
+ "type": "string"
30
+ }
31
+ },
32
+ "required": [
33
+ "filename",
34
+ "uri",
35
+ "sha256"
36
+ ]
37
+ },
38
+ "arguments": {
39
+ "type": "string"
40
+ },
41
+ "target": { "$ref": "partial:target-any" }
42
+ },
43
+ "required": ["script", "target"]
44
+ }
@@ -23,6 +23,7 @@ module BoltServer
23
23
  action-check_node_connections
24
24
  action-run_command
25
25
  action-run_task
26
+ action-run_script
26
27
  action-upload_file
27
28
  transport-ssh
28
29
  transport-winrm
@@ -139,7 +140,7 @@ module BoltServer
139
140
  # but this is to be on the safe side.
140
141
  parent = File.dirname(path)
141
142
  FileUtils.mkdir_p(parent)
142
- @file_cache.download_file(path, sha256, uri)
143
+ @file_cache.serial_execute { @file_cache.download_file(path, sha256, uri) }
143
144
  elsif kind == 'directory'
144
145
  # Create directory in cache so we can move files in.
145
146
  FileUtils.mkdir_p(path)
@@ -148,7 +149,28 @@ module BoltServer
148
149
  'boltserver/schema-error').to_json]
149
150
  end
150
151
  end
151
- [@executor.upload_file(target, cache_dir, destination), nil]
152
+ # We need to special case the scenario where only one file was
153
+ # included in the request to download. Otherwise, the call to upload_file
154
+ # will attempt to upload with a directory as a source and potentially a
155
+ # filename as a destination on the host. In that case the end result will
156
+ # be the file downloaded to a directory with the same name as the source
157
+ # filename, rather than directly to the filename set in the destination.
158
+ upload_source = if files.size == 1 && files[0]['kind'] == 'file'
159
+ File.join(cache_dir, files[0]['relative_path'])
160
+ else
161
+ cache_dir
162
+ end
163
+ [@executor.upload_file(target, upload_source, destination), nil]
164
+ end
165
+
166
+ def run_script(target, body)
167
+ error = validate_schema(@schemas["action-run_script"], body)
168
+ return [], error unless error.nil?
169
+
170
+ # Download the file onto the machine.
171
+ file_location = @file_cache.update_file(body['script'])
172
+
173
+ [@executor.run_script(target, file_location, body['arguments'])]
152
174
  end
153
175
 
154
176
  get '/' do
@@ -174,6 +196,7 @@ module BoltServer
174
196
  check_node_connections
175
197
  run_command
176
198
  run_task
199
+ run_script
177
200
  upload_file
178
201
  ].freeze
179
202