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.
- checksums.yaml +4 -4
- data/bolt-modules/boltlib/lib/puppet/functions/fail_plan.rb +2 -3
- data/bolt-modules/boltlib/lib/puppet/functions/file_upload.rb +2 -2
- data/bolt-modules/boltlib/lib/puppet/functions/get_targets.rb +2 -3
- data/bolt-modules/boltlib/lib/puppet/functions/run_command.rb +2 -2
- data/bolt-modules/boltlib/lib/puppet/functions/run_plan.rb +2 -2
- data/bolt-modules/boltlib/lib/puppet/functions/run_task.rb +2 -2
- data/bolt-modules/boltlib/lib/puppet/functions/set_var.rb +29 -0
- data/bolt-modules/boltlib/lib/puppet/functions/vars.rb +27 -0
- data/lib/bolt/cli.rb +220 -184
- data/lib/bolt/config.rb +95 -13
- data/lib/bolt/executor.rb +12 -5
- data/lib/bolt/inventory.rb +48 -11
- data/lib/bolt/inventory/group.rb +22 -2
- data/lib/bolt/logger.rb +56 -8
- data/lib/bolt/outputter/human.rb +18 -1
- data/lib/bolt/pal.rb +6 -1
- data/lib/bolt/target.rb +1 -1
- data/lib/bolt/transport/base.rb +3 -0
- data/lib/bolt/transport/local.rb +90 -0
- data/lib/bolt/transport/local/shell.rb +29 -0
- data/lib/bolt/transport/orch.rb +2 -2
- data/lib/bolt/transport/ssh.rb +0 -3
- data/lib/bolt/transport/winrm.rb +0 -3
- data/lib/bolt/util.rb +31 -4
- data/lib/bolt/version.rb +1 -1
- data/lib/bolt_ext/puppetdb_inventory.rb +9 -2
- data/modules/aggregate/lib/puppet/functions/aggregate/count.rb +19 -0
- data/modules/aggregate/lib/puppet/functions/aggregate/nodes.rb +19 -0
- data/modules/aggregate/plans/count.pp +35 -0
- data/modules/aggregate/plans/nodes.pp +35 -0
- data/modules/canary/lib/puppet/functions/canary/merge.rb +11 -0
- data/modules/canary/lib/puppet/functions/canary/random_split.rb +20 -0
- data/modules/canary/lib/puppet/functions/canary/skip.rb +23 -0
- data/modules/canary/plans/init.pp +52 -0
- metadata +14 -16
data/lib/bolt/logger.rb
CHANGED
@@ -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
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
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
|
-
|
25
|
-
|
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
|
data/lib/bolt/outputter/human.rb
CHANGED
@@ -72,7 +72,24 @@ module Bolt
|
|
72
72
|
end
|
73
73
|
|
74
74
|
def print_summary(results, elapsed_time)
|
75
|
-
|
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)
|
data/lib/bolt/pal.rb
CHANGED
@@ -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:
|
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
|
data/lib/bolt/target.rb
CHANGED
@@ -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
|
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
|
data/lib/bolt/transport/base.rb
CHANGED
@@ -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
|
data/lib/bolt/transport/orch.rb
CHANGED
@@ -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[:
|
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[:
|
120
|
+
[target.options[:"task-environment"],
|
121
121
|
target.options[:"service-url"],
|
122
122
|
target.options[:"token-file"]]
|
123
123
|
end.values
|
data/lib/bolt/transport/ssh.rb
CHANGED
data/lib/bolt/transport/winrm.rb
CHANGED
@@ -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
|
data/lib/bolt/util.rb
CHANGED
@@ -43,10 +43,37 @@ module Bolt
|
|
43
43
|
hash1.merge(hash2, &recursive_merge)
|
44
44
|
end
|
45
45
|
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
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
|
data/lib/bolt/version.rb
CHANGED
@@ -9,7 +9,12 @@ module Bolt
|
|
9
9
|
class PuppetDBInventory
|
10
10
|
class Client
|
11
11
|
def self.from_config(config)
|
12
|
-
uri =
|
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 "
|
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
|
+
}
|