bolt 0.16.4 → 0.17.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 (36) hide show
  1. checksums.yaml +4 -4
  2. data/bolt-modules/boltlib/lib/puppet/functions/fail_plan.rb +2 -3
  3. data/bolt-modules/boltlib/lib/puppet/functions/file_upload.rb +2 -2
  4. data/bolt-modules/boltlib/lib/puppet/functions/get_targets.rb +2 -3
  5. data/bolt-modules/boltlib/lib/puppet/functions/run_command.rb +2 -2
  6. data/bolt-modules/boltlib/lib/puppet/functions/run_plan.rb +2 -2
  7. data/bolt-modules/boltlib/lib/puppet/functions/run_task.rb +2 -2
  8. data/bolt-modules/boltlib/lib/puppet/functions/set_var.rb +29 -0
  9. data/bolt-modules/boltlib/lib/puppet/functions/vars.rb +27 -0
  10. data/lib/bolt/cli.rb +220 -184
  11. data/lib/bolt/config.rb +95 -13
  12. data/lib/bolt/executor.rb +12 -5
  13. data/lib/bolt/inventory.rb +48 -11
  14. data/lib/bolt/inventory/group.rb +22 -2
  15. data/lib/bolt/logger.rb +56 -8
  16. data/lib/bolt/outputter/human.rb +18 -1
  17. data/lib/bolt/pal.rb +6 -1
  18. data/lib/bolt/target.rb +1 -1
  19. data/lib/bolt/transport/base.rb +3 -0
  20. data/lib/bolt/transport/local.rb +90 -0
  21. data/lib/bolt/transport/local/shell.rb +29 -0
  22. data/lib/bolt/transport/orch.rb +2 -2
  23. data/lib/bolt/transport/ssh.rb +0 -3
  24. data/lib/bolt/transport/winrm.rb +0 -3
  25. data/lib/bolt/util.rb +31 -4
  26. data/lib/bolt/version.rb +1 -1
  27. data/lib/bolt_ext/puppetdb_inventory.rb +9 -2
  28. data/modules/aggregate/lib/puppet/functions/aggregate/count.rb +19 -0
  29. data/modules/aggregate/lib/puppet/functions/aggregate/nodes.rb +19 -0
  30. data/modules/aggregate/plans/count.pp +35 -0
  31. data/modules/aggregate/plans/nodes.pp +35 -0
  32. data/modules/canary/lib/puppet/functions/canary/merge.rb +11 -0
  33. data/modules/canary/lib/puppet/functions/canary/random_split.rb +20 -0
  34. data/modules/canary/lib/puppet/functions/canary/skip.rb +23 -0
  35. data/modules/canary/plans/init.pp +52 -0
  36. metadata +14 -16
@@ -13,16 +13,64 @@ module Bolt
13
13
  return if Logging.initialized?
14
14
 
15
15
  Logging.init :debug, :info, :notice, :warn, :error, :fatal, :any
16
- Logging.appenders.stderr(
17
- 'stderr',
18
- layout: Logging.layouts.pattern(
19
- pattern: '%d %-6l %c: %m\n',
20
- date_pattern: '%Y-%m-%dT%H:%M:%S.%6N'
21
- )
16
+
17
+ root_logger = Logging.logger[:root]
18
+ root_logger.add_appenders Logging.appenders.stderr(
19
+ 'console',
20
+ layout: default_layout,
21
+ level: default_level
22
22
  )
23
+ # We set the root logger's level so that it logs everything but we do
24
+ # limit what's actually logged in every appender individually.
25
+ root_logger.level = :all
26
+ end
27
+
28
+ def self.configure(config)
23
29
  root_logger = Logging.logger[:root]
24
- root_logger.add_appenders :stderr
25
- root_logger.level = :notice
30
+
31
+ config[:log].each_pair do |name, params|
32
+ appender = Logging.appenders[name]
33
+ if appender.nil?
34
+ unless name.start_with?('file:')
35
+ raise Bolt::Error.new("Unexpected log: #{name}", 'bolt/internal-error')
36
+ end
37
+
38
+ begin
39
+ appender = Logging.appenders.file(
40
+ name,
41
+ filename: name[5..-1], # strip the "file:" prefix
42
+ truncate: (params[:append] == false),
43
+ layout: default_layout,
44
+ level: default_level
45
+ )
46
+ rescue ArgumentError => e
47
+ raise Bolt::CLIError, "Failed to open log #{name}: #{e.message}"
48
+ end
49
+
50
+ root_logger.add_appenders appender
51
+ end
52
+
53
+ appender.level = params[:level] if params[:level]
54
+ end
55
+ end
56
+
57
+ def self.default_layout
58
+ @default_layout ||= Logging.layouts.pattern(
59
+ pattern: '%d %-6l %c: %m\n',
60
+ date_pattern: '%Y-%m-%dT%H:%M:%S.%6N'
61
+ )
62
+ end
63
+
64
+ def self.default_level
65
+ :notice
66
+ end
67
+
68
+ def self.valid_level?(level)
69
+ !Logging.level_num(level).nil?
70
+ end
71
+
72
+ def self.levels
73
+ Logging::LNAMES.map(&:downcase)
26
74
  end
27
75
 
28
76
  def self.reset_logging
@@ -72,7 +72,24 @@ module Bolt
72
72
  end
73
73
 
74
74
  def print_summary(results, elapsed_time)
75
- @stream.puts format("Ran on %d node%s in %.2f seconds",
75
+ ok_set = results.ok_set
76
+ unless ok_set.empty?
77
+ @stream.puts format('Successful on %d node%s: %s',
78
+ ok_set.size,
79
+ ok_set.size == 1 ? '' : 's',
80
+ ok_set.names.join(','))
81
+ end
82
+
83
+ error_set = results.error_set
84
+ unless error_set.empty?
85
+ @stream.puts colorize(:red,
86
+ format('Failed on %d node%s: %s',
87
+ error_set.size,
88
+ error_set.size == 1 ? '' : 's',
89
+ error_set.names.join(',')))
90
+ end
91
+
92
+ @stream.puts format('Ran on %d node%s in %.2f seconds',
76
93
  results.size,
77
94
  results.size == 1 ? '' : 's',
78
95
  elapsed_time)
@@ -4,6 +4,7 @@ require 'bolt/error'
4
4
  module Bolt
5
5
  class PAL
6
6
  BOLTLIB_PATH = File.join(__FILE__, '../../../bolt-modules')
7
+ MODULES_PATH = File.join(__FILE__, '../../../modules')
7
8
 
8
9
  def initialize(config)
9
10
  # Nothing works without initialized this global state. Reinitializing
@@ -55,12 +56,16 @@ module Bolt
55
56
  compiler.evaluate_string('type TargetSpec = Boltlib::TargetSpec')
56
57
  end
57
58
 
59
+ def full_modulepath(modulepath)
60
+ [BOLTLIB_PATH, *modulepath, MODULES_PATH]
61
+ end
62
+
58
63
  # Runs a block in a PAL script compiler configured for Bolt. Catches
59
64
  # exceptions thrown by the block and re-raises them ensuring they are
60
65
  # Bolt::Errors since the script compiler block will squash all exceptions.
61
66
  def in_bolt_compiler(opts = [])
62
67
  Puppet.initialize_settings(opts)
63
- r = Puppet::Pal.in_tmp_environment('bolt', modulepath: [BOLTLIB_PATH] + @config[:modulepath], facts: {}) do |pal|
68
+ r = Puppet::Pal.in_tmp_environment('bolt', modulepath: full_modulepath(@config[:modulepath]), facts: {}) do |pal|
64
69
  pal.with_script_compiler do |compiler|
65
70
  add_target_spec(compiler)
66
71
  begin
@@ -58,7 +58,7 @@ module Bolt
58
58
  @uri_obj.hostname
59
59
  end
60
60
 
61
- # name is currently just uri but should be be used instead to identify the
61
+ # name is currently just uri but should be used instead to identify the
62
62
  # Target ouside the transport or uri options.
63
63
  def name
64
64
  uri
@@ -34,6 +34,9 @@ module Bolt
34
34
  # before executing, and a :node_result event for each Target after
35
35
  # execution.
36
36
  class Base
37
+ STDIN_METHODS = %w[both stdin].freeze
38
+ ENVIRONMENT_METHODS = %w[both environment].freeze
39
+
37
40
  attr_reader :logger
38
41
 
39
42
  def initialize(_config)
@@ -0,0 +1,90 @@
1
+ require 'json'
2
+ require 'fileutils'
3
+ require 'tmpdir'
4
+ require 'bolt/transport/base'
5
+ require 'bolt/result'
6
+
7
+ module Bolt
8
+ module Transport
9
+ class Local < Base
10
+ def initialize(config = nil)
11
+ super(config)
12
+
13
+ if Gem.win_platform?
14
+ raise NotImplementedError, "The local transport is not yet implemented on Windows"
15
+ else
16
+ @conn = Shell.new
17
+ end
18
+ end
19
+
20
+ def in_tmpdir(base)
21
+ args = base ? [nil, base] : []
22
+ Dir.mktmpdir(*args) do |dir|
23
+ Dir.chdir(dir) do
24
+ yield dir
25
+ end
26
+ end
27
+ rescue StandardError => e
28
+ raise Bolt::Node::FileError.new("Could not make tempdir: #{e.message}", 'TEMPDIR_ERROR')
29
+ end
30
+ private :in_tmpdir
31
+
32
+ def copy_file(source, destination)
33
+ FileUtils.copy_file(source, destination)
34
+ rescue StandardError => e
35
+ raise Bolt::Node::FileError.new(e.message, 'WRITE_ERROR')
36
+ end
37
+
38
+ def with_tmpscript(script, base)
39
+ in_tmpdir(base) do |dir|
40
+ dest = File.join(dir, File.basename(script))
41
+ copy_file(script, dest)
42
+ File.chmod(0o750, dest)
43
+ yield dest
44
+ end
45
+ end
46
+ private :with_tmpscript
47
+
48
+ def upload(target, source, destination, _options = {})
49
+ copy_file(source, destination)
50
+ Bolt::Result.for_upload(target, source, destination)
51
+ end
52
+
53
+ def run_command(target, command, _options = {})
54
+ in_tmpdir(target.options[:tmpdir]) do |_|
55
+ output = @conn.execute(command, {})
56
+ Bolt::Result.for_command(target, output.stdout.string, output.stderr.string, output.exit_code)
57
+ end
58
+ end
59
+
60
+ def run_script(target, script, arguments, _options = {})
61
+ with_tmpscript(File.absolute_path(script), target.options[:tmpdir]) do |file|
62
+ logger.debug "Running '#{file}' with #{arguments}"
63
+
64
+ if arguments.empty?
65
+ # We will always provide separated arguments, so work-around Open3's handling of a single
66
+ # argument as the entire command string for script paths containing spaces.
67
+ arguments = ['']
68
+ end
69
+ output = @conn.execute(file, *arguments, {})
70
+ Bolt::Result.for_command(target, output.stdout.string, output.stderr.string, output.exit_code)
71
+ end
72
+ end
73
+
74
+ def run_task(target, task, arguments, _options = {})
75
+ input_method = task.input_method
76
+ stdin = STDIN_METHODS.include?(input_method) ? JSON.dump(arguments) : nil
77
+ env = ENVIRONMENT_METHODS.include?(input_method) ? arguments : nil
78
+
79
+ with_tmpscript(task.executable, target.options[:tmpdir]) do |script|
80
+ logger.debug("Running '#{script}' with #{arguments}")
81
+
82
+ output = @conn.execute(script, stdin: stdin, env: env)
83
+ Bolt::Result.for_task(target, output.stdout.string, output.stderr.string, output.exit_code)
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
89
+
90
+ require 'bolt/transport/local/shell'
@@ -0,0 +1,29 @@
1
+ require 'open3'
2
+ require 'bolt/node/output'
3
+
4
+ module Bolt
5
+ module Transport
6
+ class Local
7
+ class Shell
8
+ def execute(*command, options)
9
+ if options[:env]
10
+ env = options[:env].each_with_object({}) { |(k, v), h| h["PT_#{k}"] = v }
11
+ command = [env] + command
12
+ end
13
+
14
+ if options[:stdin]
15
+ stdout, stderr, rc = Open3.capture3(*command, stdin_data: options[:stdin])
16
+ else
17
+ stdout, stderr, rc = Open3.capture3(*command)
18
+ end
19
+
20
+ result_output = Bolt::Node::Output.new
21
+ result_output.stdout << stdout unless stdout.nil?
22
+ result_output.stderr << stderr unless stderr.nil?
23
+ result_output.exit_code = rc.to_i
24
+ result_output
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -27,7 +27,7 @@ module Bolt
27
27
 
28
28
  def build_request(targets, task, arguments)
29
29
  { task: task.name,
30
- environment: targets.first.options[:orch_task_environment],
30
+ environment: targets.first.options[:"task-environment"],
31
31
  noop: arguments['_noop'],
32
32
  params: arguments.reject { |k, _| k == '_noop' },
33
33
  scope: {
@@ -117,7 +117,7 @@ module Bolt
117
117
 
118
118
  def batches(targets)
119
119
  targets.group_by do |target|
120
- [target.options[:orch_task_environment],
120
+ [target.options[:"task-environment"],
121
121
  target.options[:"service-url"],
122
122
  target.options[:"token-file"]]
123
123
  end.values
@@ -6,9 +6,6 @@ require 'shellwords'
6
6
  module Bolt
7
7
  module Transport
8
8
  class SSH < Base
9
- STDIN_METHODS = %w[both stdin].freeze
10
- ENVIRONMENT_METHODS = %w[both environment].freeze
11
-
12
9
  def initialize(_config)
13
10
  super
14
11
 
@@ -3,9 +3,6 @@ require 'bolt/transport/base'
3
3
  module Bolt
4
4
  module Transport
5
5
  class WinRM < Base
6
- STDIN_METHODS = %w[both stdin].freeze
7
- ENVIRONMENT_METHODS = %w[both environment].freeze
8
-
9
6
  PS_ARGS = %w[
10
7
  -NoProfile -NonInteractive -NoLogo -ExecutionPolicy Bypass
11
8
  ].freeze
@@ -43,10 +43,37 @@ module Bolt
43
43
  hash1.merge(hash2, &recursive_merge)
44
44
  end
45
45
 
46
- def symbolize_keys(hash)
47
- hash.each_with_object({}) do |(k, v), acc|
48
- k = k.to_sym
49
- acc[k] = v.is_a?(Hash) ? symbolize_keys(v) : v
46
+ # Performs a deep_clone, using an identical copy if the cloned structure contains multiple
47
+ # references to the same object and prevents endless recursion.
48
+ # Credit to Jan Molic via https://github.com/rubyworks/facets/blob/master/LICENSE.txt
49
+ def deep_clone(obj, cloned = {})
50
+ cloned[obj.object_id] if cloned.include?(obj.object_id)
51
+
52
+ begin
53
+ cl = obj.clone
54
+ rescue TypeError
55
+ # unclonable (TrueClass, Fixnum, ...)
56
+ cloned[obj.object_id] = obj
57
+ return obj
58
+ else
59
+ cloned[obj.object_id] = cl
60
+ cloned[cl.object_id] = cl
61
+
62
+ if cl.is_a? Hash
63
+ obj.each { |k, v| cl[k] = deep_clone(v, cloned) }
64
+ elsif cl.is_a? Array
65
+ cl.collect! { |v| deep_clone(v, cloned) }
66
+ elsif cl.is_a? Struct
67
+ obj.each_pair { |k, v| cl[k] = deep_clone(v, cloned) }
68
+ end
69
+
70
+ cl.instance_variables.each do |var|
71
+ v = cl.instance_eval { var }
72
+ v_cl = deep_clone(v, cloned)
73
+ cl.instance_eval { @var = v_cl }
74
+ end
75
+
76
+ return cl
50
77
  end
51
78
  end
52
79
  end
@@ -1,3 +1,3 @@
1
1
  module Bolt
2
- VERSION = '0.16.4'.freeze
2
+ VERSION = '0.17.0'.freeze
3
3
  end
@@ -9,7 +9,12 @@ module Bolt
9
9
  class PuppetDBInventory
10
10
  class Client
11
11
  def self.from_config(config)
12
- uri = URI.parse(config['server_urls'].first)
12
+ uri = if config['server_urls'].is_a? String
13
+ config['server_urls']
14
+ else
15
+ config['server_urls'].first
16
+ end
17
+ uri = URI.parse(uri)
13
18
  uri.port ||= 8081
14
19
 
15
20
  cacert = File.expand_path(config['cacert'])
@@ -83,6 +88,8 @@ module Bolt
83
88
  end
84
89
  elsif File.exist?(DEFAULT_CONFIG)
85
90
  config = JSON.parse(File.read(DEFAULT_CONFIG))
91
+ else
92
+ config = {}
86
93
  end
87
94
  config.fetch('puppetdb', {})
88
95
  end
@@ -197,7 +204,7 @@ query results.
197
204
 
198
205
  inventory_file = positional_args.shift
199
206
  unless inventory_file
200
- raise "--inventory is a required option"
207
+ raise "Please specify an input file (see --help for details)"
201
208
  end
202
209
 
203
210
  if positional_args.any?
@@ -0,0 +1,19 @@
1
+ # Aggregates the key/value pairs in the results of a ResultSet into a hash
2
+ # mapping the keys to a hash of each distinct value and how many nodes returned
3
+ # that value for the key.
4
+ Puppet::Functions.create_function(:'aggregate::count') do
5
+ dispatch :aggregate_count do
6
+ param 'ResultSet', :resultset
7
+ end
8
+
9
+ def aggregate_count(resultset)
10
+ resultset.each_with_object({}) do |result, agg|
11
+ result.value.each do |key, val|
12
+ agg[key] ||= {}
13
+ agg[key][val.to_s] ||= 0
14
+ agg[key][val.to_s] += 1
15
+ end
16
+ agg
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,19 @@
1
+ # Aggregates the key/value pairs in the results of a ResultSet into a hash
2
+ # mapping the keys to a hash of each distinct value and the list of nodes
3
+ # returning that value for the key.
4
+ Puppet::Functions.create_function(:'aggregate::nodes') do
5
+ dispatch :aggregate_nodes do
6
+ param 'ResultSet', :resultset
7
+ end
8
+
9
+ def aggregate_nodes(resultset)
10
+ resultset.each_with_object({}) do |result, agg|
11
+ result.value.each do |key, val|
12
+ agg[key] ||= {}
13
+ agg[key][val.to_s] ||= []
14
+ agg[key][val.to_s] << result.target.name
15
+ end
16
+ agg
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,35 @@
1
+ plan aggregate::count(
2
+ Optional[String[0]] $task = undef,
3
+ Optional[String[0]] $command = undef,
4
+ Optional[String[0]] $script = undef,
5
+ TargetSpec $nodes,
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", 'canary/invalid-params')
20
+ }
21
+
22
+ if ($type_count > 1) {
23
+ fail_plan("Must specify only one command, script, or task to run", 'canary/invalid-params')
24
+ }
25
+
26
+ $res = if ($task) {
27
+ run_task($task, $nodes, $params)
28
+ } elsif ($command) {
29
+ run_command($command, $nodes, $params)
30
+ } elsif ($script) {
31
+ run_script($script, $nodes, $params)
32
+ }
33
+
34
+ aggregate::count($res)
35
+ }