bolt 1.15.0 → 1.16.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/datatypes/target.rb +1 -1
- data/bolt-modules/boltlib/lib/puppet/functions/run_plan.rb +4 -4
- data/lib/bolt.rb +3 -0
- data/lib/bolt/analytics.rb +7 -2
- data/lib/bolt/applicator.rb +6 -2
- data/lib/bolt/bolt_option_parser.rb +4 -4
- data/lib/bolt/cli.rb +8 -4
- data/lib/bolt/config.rb +6 -6
- data/lib/bolt/executor.rb +2 -7
- data/lib/bolt/inventory.rb +37 -6
- data/lib/bolt/inventory/group2.rb +314 -0
- data/lib/bolt/inventory/inventory2.rb +261 -0
- data/lib/bolt/outputter/human.rb +3 -1
- data/lib/bolt/pal.rb +8 -7
- data/lib/bolt/puppetdb/client.rb +6 -5
- data/lib/bolt/target.rb +34 -14
- data/lib/bolt/task.rb +2 -2
- data/lib/bolt/transport/base.rb +2 -2
- data/lib/bolt/transport/docker.rb +1 -1
- data/lib/bolt/transport/docker/connection.rb +2 -0
- data/lib/bolt/transport/local.rb +9 -181
- data/lib/bolt/transport/local/shell.rb +202 -12
- data/lib/bolt/transport/local_windows.rb +203 -0
- data/lib/bolt/transport/orch.rb +6 -4
- data/lib/bolt/transport/orch/connection.rb +6 -2
- data/lib/bolt/transport/ssh.rb +10 -150
- data/lib/bolt/transport/ssh/connection.rb +15 -116
- data/lib/bolt/transport/sudoable.rb +163 -0
- data/lib/bolt/transport/sudoable/connection.rb +76 -0
- data/lib/bolt/transport/sudoable/tmpdir.rb +59 -0
- data/lib/bolt/transport/winrm.rb +4 -4
- data/lib/bolt/transport/winrm/connection.rb +1 -0
- data/lib/bolt/util.rb +2 -0
- data/lib/bolt/version.rb +1 -1
- data/lib/bolt_ext/puppetdb_inventory.rb +0 -1
- data/lib/bolt_server/transport_app.rb +3 -1
- data/lib/logging_extensions/logging.rb +13 -0
- data/lib/plan_executor/orch_client.rb +4 -0
- metadata +23 -2
@@ -0,0 +1,261 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bolt/inventory/group2'
|
4
|
+
|
5
|
+
module Bolt
|
6
|
+
class Inventory
|
7
|
+
class Inventory2
|
8
|
+
def initialize(data, config = nil, target_vars: {}, target_facts: {}, target_features: {})
|
9
|
+
@logger = Logging.logger[self]
|
10
|
+
# Config is saved to add config options to targets
|
11
|
+
@config = config || Bolt::Config.default
|
12
|
+
@data = data ||= {}
|
13
|
+
@groups = Group2.new(data.merge('name' => 'all'))
|
14
|
+
@group_lookup = {}
|
15
|
+
@target_vars = target_vars
|
16
|
+
@target_facts = target_facts
|
17
|
+
@target_features = target_features
|
18
|
+
|
19
|
+
@groups.resolve_aliases(@groups.node_aliases, @groups.node_names)
|
20
|
+
collect_groups
|
21
|
+
end
|
22
|
+
|
23
|
+
def validate
|
24
|
+
@groups.validate
|
25
|
+
end
|
26
|
+
|
27
|
+
def collect_groups
|
28
|
+
# Provide a lookup map for finding a group by name
|
29
|
+
@group_lookup = @groups.collect_groups
|
30
|
+
end
|
31
|
+
|
32
|
+
def group_names
|
33
|
+
@group_lookup.keys
|
34
|
+
end
|
35
|
+
|
36
|
+
def node_names
|
37
|
+
@groups.node_names
|
38
|
+
end
|
39
|
+
|
40
|
+
def get_targets(targets)
|
41
|
+
targets = expand_targets(targets)
|
42
|
+
targets = if targets.is_a? Array
|
43
|
+
targets.flatten.uniq(&:name)
|
44
|
+
else
|
45
|
+
[targets]
|
46
|
+
end
|
47
|
+
targets.map { |t| update_target(t) }
|
48
|
+
end
|
49
|
+
|
50
|
+
def add_to_group(targets, desired_group)
|
51
|
+
if group_names.include?(desired_group)
|
52
|
+
targets.each do |target|
|
53
|
+
if group_names.include?(target.name)
|
54
|
+
raise ValidationError.new("Group #{target.name} conflicts with node of the same name", target.name)
|
55
|
+
end
|
56
|
+
add_node(@groups, target, desired_group)
|
57
|
+
end
|
58
|
+
else
|
59
|
+
raise ValidationError.new("Group #{desired_group} does not exist in inventory", nil)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def set_var(target, key, value)
|
64
|
+
data = { key => value }
|
65
|
+
set_vars_from_hash(target.name, data)
|
66
|
+
end
|
67
|
+
|
68
|
+
def vars(target)
|
69
|
+
@target_vars[target.name] || {}
|
70
|
+
end
|
71
|
+
|
72
|
+
def add_facts(target, new_facts = {})
|
73
|
+
@logger.warn("No facts to add") if new_facts.empty?
|
74
|
+
set_facts(target.name, new_facts)
|
75
|
+
end
|
76
|
+
|
77
|
+
def facts(target)
|
78
|
+
@target_facts[target.name] || {}
|
79
|
+
end
|
80
|
+
|
81
|
+
def set_feature(target, feature, value = true)
|
82
|
+
@target_features[target.name] ||= Set.new
|
83
|
+
if value
|
84
|
+
@target_features[target.name] << feature
|
85
|
+
else
|
86
|
+
@target_features[target.name].delete(feature)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def features(target)
|
91
|
+
@target_features[target.name] || Set.new
|
92
|
+
end
|
93
|
+
|
94
|
+
def data_hash
|
95
|
+
{
|
96
|
+
data: @data,
|
97
|
+
target_hash: {
|
98
|
+
target_vars: @target_vars,
|
99
|
+
target_facts: @target_facts,
|
100
|
+
target_features: @target_features
|
101
|
+
},
|
102
|
+
config: @config.transport_data_get
|
103
|
+
}
|
104
|
+
end
|
105
|
+
|
106
|
+
#### PRIVATE ####
|
107
|
+
#
|
108
|
+
# For debugging only now
|
109
|
+
def groups_in(node_name)
|
110
|
+
@groups.data_for(node_name)['groups'] || {}
|
111
|
+
end
|
112
|
+
private :groups_in
|
113
|
+
|
114
|
+
# Pass a target to get_targets for a public version of this
|
115
|
+
# Should this reconfigure configured targets?
|
116
|
+
def update_target(target)
|
117
|
+
data = @groups.data_for(target.name)
|
118
|
+
data ||= {}
|
119
|
+
|
120
|
+
unless data['config']
|
121
|
+
@logger.debug("Did not find config for #{target.name} in inventory")
|
122
|
+
data['config'] = {}
|
123
|
+
end
|
124
|
+
|
125
|
+
data = Bolt::Inventory.localhost_defaults(data) if target.name == 'localhost'
|
126
|
+
# These should only get set from the inventory if they have not yet
|
127
|
+
# been instantiated
|
128
|
+
set_vars_from_hash(target.name, data['vars']) unless @target_vars[target.name]
|
129
|
+
set_facts(target.name, data['facts']) unless @target_facts[target.name]
|
130
|
+
data['features']&.each { |feature| set_feature(target, feature) } unless @target_features[target.name]
|
131
|
+
|
132
|
+
# Use Config object to ensure config section is treated consistently with config file
|
133
|
+
conf = @config.deep_clone
|
134
|
+
conf.update_from_inventory(data['config'])
|
135
|
+
conf.validate
|
136
|
+
|
137
|
+
target.update_conf(conf.transport_conf)
|
138
|
+
|
139
|
+
unless target.transport.nil? || Bolt::TRANSPORTS.include?(target.transport.to_sym)
|
140
|
+
raise Bolt::UnknownTransportError.new(target.transport, target.uri)
|
141
|
+
end
|
142
|
+
|
143
|
+
target
|
144
|
+
end
|
145
|
+
private :update_target
|
146
|
+
|
147
|
+
# If target is a group name, expand it to the members of that group.
|
148
|
+
# Else match against nodes in inventory by name or alias.
|
149
|
+
# If a wildcard string, error if no matches are found.
|
150
|
+
# Else fall back to [target] if no matches are found.
|
151
|
+
def resolve_name(target)
|
152
|
+
if (group = @group_lookup[target])
|
153
|
+
group.node_names
|
154
|
+
else
|
155
|
+
# Try to wildcard match nodes in inventory
|
156
|
+
# Ignore case because hostnames are generally case-insensitive
|
157
|
+
regexp = Regexp.new("^#{Regexp.escape(target).gsub('\*', '.*?')}$", Regexp::IGNORECASE)
|
158
|
+
|
159
|
+
nodes = @groups.node_names.select { |node| node =~ regexp }
|
160
|
+
nodes += @groups.node_aliases.select { |target_alias, _node| target_alias =~ regexp }.values
|
161
|
+
|
162
|
+
if nodes.empty?
|
163
|
+
raise(WildcardError, target) if target.include?('*')
|
164
|
+
[target]
|
165
|
+
else
|
166
|
+
nodes
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
170
|
+
private :resolve_name
|
171
|
+
|
172
|
+
def expand_targets(targets)
|
173
|
+
if targets.is_a? Bolt::Target
|
174
|
+
targets.inventory = self
|
175
|
+
targets
|
176
|
+
elsif targets.is_a? Array
|
177
|
+
targets.map { |tish| expand_targets(tish) }
|
178
|
+
elsif targets.is_a? String
|
179
|
+
# Expand a comma-separated list
|
180
|
+
targets.split(/[[:space:],]+/).reject(&:empty?).map do |name|
|
181
|
+
ts = resolve_name(name)
|
182
|
+
ts.map do |t|
|
183
|
+
target = create_target(t)
|
184
|
+
target.inventory = self
|
185
|
+
target
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
190
|
+
private :expand_targets
|
191
|
+
|
192
|
+
def set_vars_from_hash(tname, data)
|
193
|
+
if data
|
194
|
+
# Instantiate empty vars hash in case no vars are defined
|
195
|
+
@target_vars[tname] ||= {}
|
196
|
+
# Assign target new merged vars hash
|
197
|
+
# This is essentially a copy-on-write to maintain the immutability of @target_vars
|
198
|
+
@target_vars[tname] = @target_vars[tname].merge(data).freeze
|
199
|
+
end
|
200
|
+
end
|
201
|
+
private :set_vars_from_hash
|
202
|
+
|
203
|
+
def set_facts(tname, hash)
|
204
|
+
if hash
|
205
|
+
@target_facts[tname] ||= {}
|
206
|
+
@target_facts[tname] = Bolt::Util.deep_merge(@target_facts[tname], hash).freeze
|
207
|
+
end
|
208
|
+
end
|
209
|
+
private :set_facts
|
210
|
+
|
211
|
+
def add_node(current_group, target, desired_group, track = { 'all' => nil })
|
212
|
+
if current_group.name == desired_group
|
213
|
+
# Group to add to is found
|
214
|
+
t_name = target.name
|
215
|
+
# Add target to nodes hash
|
216
|
+
current_group.nodes[t_name] = { 'name' => t_name }.merge(target.options)
|
217
|
+
# Inherit facts, vars, and features from hierarchy
|
218
|
+
current_group_data = { facts: current_group.facts,
|
219
|
+
vars: current_group.vars,
|
220
|
+
features: current_group.features }
|
221
|
+
data = inherit_data(track, current_group.name, current_group_data)
|
222
|
+
set_facts(t_name, @target_facts[t_name] ? data[:facts].merge(@target_facts[t_name]) : data[:facts])
|
223
|
+
set_vars_from_hash(t_name, @target_vars[t_name] ? data[:vars].merge(@target_vars[t_name]) : data[:vars])
|
224
|
+
data[:features].each do |feature|
|
225
|
+
set_feature(target, feature)
|
226
|
+
end
|
227
|
+
return true
|
228
|
+
end
|
229
|
+
# Recurse on children Groups if not desired_group
|
230
|
+
current_group.groups.each do |child_group|
|
231
|
+
track[child_group.name] = current_group
|
232
|
+
add_node(child_group, target, desired_group, track)
|
233
|
+
end
|
234
|
+
end
|
235
|
+
private :add_node
|
236
|
+
|
237
|
+
def inherit_data(track, name, data)
|
238
|
+
unless track[name].nil?
|
239
|
+
data[:facts] = track[name].facts.merge(data[:facts])
|
240
|
+
data[:vars] = track[name].vars.merge(data[:vars])
|
241
|
+
data[:features].concat(track[name].features)
|
242
|
+
inherit_data(track, track[name].name, data)
|
243
|
+
end
|
244
|
+
data
|
245
|
+
end
|
246
|
+
private :inherit_data
|
247
|
+
|
248
|
+
def create_target(target_name)
|
249
|
+
data = @groups.data_for(target_name) || {}
|
250
|
+
name_opt = {}
|
251
|
+
name_opt['name'] = data['name'] if data['name']
|
252
|
+
|
253
|
+
# If there is no name then this node was only referred to as a string.
|
254
|
+
uri = data['uri']
|
255
|
+
uri ||= target_name unless data['name']
|
256
|
+
|
257
|
+
Target.new(uri, name_opt)
|
258
|
+
end
|
259
|
+
end
|
260
|
+
end
|
261
|
+
end
|
data/lib/bolt/outputter/human.rb
CHANGED
@@ -1,6 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require 'terminal-table'
|
4
3
|
require 'bolt/pal'
|
5
4
|
|
6
5
|
module Bolt
|
@@ -101,6 +100,9 @@ module Bolt
|
|
101
100
|
end
|
102
101
|
|
103
102
|
def print_table(results)
|
103
|
+
# lazy-load expensive gem code
|
104
|
+
require 'terminal-table'
|
105
|
+
|
104
106
|
@stream.puts Terminal::Table.new(
|
105
107
|
rows: results,
|
106
108
|
style: {
|
data/lib/bolt/pal.rb
CHANGED
@@ -5,6 +5,7 @@ require 'bolt/executor'
|
|
5
5
|
require 'bolt/error'
|
6
6
|
require 'bolt/plan_result'
|
7
7
|
require 'bolt/util'
|
8
|
+
require 'etc'
|
8
9
|
|
9
10
|
module Bolt
|
10
11
|
class PAL
|
@@ -36,7 +37,7 @@ module Bolt
|
|
36
37
|
end
|
37
38
|
end
|
38
39
|
|
39
|
-
def initialize(modulepath, hiera_config, max_compiles =
|
40
|
+
def initialize(modulepath, hiera_config, max_compiles = Etc.nprocessors)
|
40
41
|
# Nothing works without initialized this global state. Reinitializing
|
41
42
|
# is safe and in practice only happen in tests
|
42
43
|
self.class.load_puppet
|
@@ -107,12 +108,12 @@ module Bolt
|
|
107
108
|
Puppet.override(yaml_plan_instantiator: Bolt::PAL::YamlPlan::Loader) do
|
108
109
|
yield compiler
|
109
110
|
end
|
110
|
-
rescue Bolt::Error =>
|
111
|
-
|
112
|
-
rescue Puppet::PreformattedError =>
|
113
|
-
PALError.from_preformatted_error(
|
114
|
-
rescue StandardError =>
|
115
|
-
PALError.from_preformatted_error(
|
111
|
+
rescue Bolt::Error => e
|
112
|
+
e
|
113
|
+
rescue Puppet::PreformattedError => e
|
114
|
+
PALError.from_preformatted_error(e)
|
115
|
+
rescue StandardError => e
|
116
|
+
PALError.from_preformatted_error(e)
|
116
117
|
end
|
117
118
|
end
|
118
119
|
end
|
data/lib/bolt/puppetdb/client.rb
CHANGED
@@ -3,7 +3,6 @@
|
|
3
3
|
require 'json'
|
4
4
|
require 'logging'
|
5
5
|
require 'uri'
|
6
|
-
require 'httpclient'
|
7
6
|
|
8
7
|
module Bolt
|
9
8
|
module PuppetDB
|
@@ -50,8 +49,8 @@ module Bolt
|
|
50
49
|
|
51
50
|
begin
|
52
51
|
response = http_client.post(url, body: body, header: headers)
|
53
|
-
rescue StandardError =>
|
54
|
-
raise Bolt::PuppetDBFailoverError, "Failed to query PuppetDB: #{
|
52
|
+
rescue StandardError => e
|
53
|
+
raise Bolt::PuppetDBFailoverError, "Failed to query PuppetDB: #{e}"
|
55
54
|
end
|
56
55
|
|
57
56
|
if response.code != 200
|
@@ -68,14 +67,16 @@ module Bolt
|
|
68
67
|
rescue JSON::ParserError
|
69
68
|
raise Bolt::PuppetDBError, "Unable to parse response as JSON: #{response.body}"
|
70
69
|
end
|
71
|
-
rescue Bolt::PuppetDBFailoverError =>
|
72
|
-
@logger.error("Request to puppetdb at #{@current_url} failed with #{
|
70
|
+
rescue Bolt::PuppetDBFailoverError => e
|
71
|
+
@logger.error("Request to puppetdb at #{@current_url} failed with #{e}.")
|
73
72
|
reject_url
|
74
73
|
make_query(query, path)
|
75
74
|
end
|
76
75
|
|
77
76
|
def http_client
|
78
77
|
return @http if @http
|
78
|
+
# lazy-load expensive gem code
|
79
|
+
require 'httpclient'
|
79
80
|
@http = HTTPClient.new
|
80
81
|
@http.ssl_config.set_client_cert_file(@config.cert, @config.key) if @config.cert
|
81
82
|
@http.ssl_config.add_trust_ca(@config.cacert)
|
data/lib/bolt/target.rb
CHANGED
@@ -1,14 +1,13 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require 'addressable/uri'
|
4
3
|
require 'bolt/error'
|
5
4
|
|
6
5
|
module Bolt
|
7
6
|
class Target
|
8
|
-
attr_reader :
|
7
|
+
attr_reader :options
|
9
8
|
# CODEREVIEW: this feels wrong. The altertative is threading inventory through the
|
10
9
|
# executor to the RemoteTransport
|
11
|
-
attr_accessor :inventory
|
10
|
+
attr_accessor :uri, :inventory
|
12
11
|
|
13
12
|
PRINT_OPTS ||= %w[host user port protocol].freeze
|
14
13
|
|
@@ -17,7 +16,11 @@ module Bolt
|
|
17
16
|
new(hash['uri'], hash['options'])
|
18
17
|
end
|
19
18
|
|
19
|
+
# URI can be passes as nil
|
20
20
|
def initialize(uri, options = nil)
|
21
|
+
# lazy-load expensive gem code
|
22
|
+
require 'addressable/uri'
|
23
|
+
|
21
24
|
@uri = uri
|
22
25
|
@uri_obj = parse(uri)
|
23
26
|
@options = options || {}
|
@@ -38,6 +41,13 @@ module Bolt
|
|
38
41
|
if @options['protocol']
|
39
42
|
@protocol = @options['protocol']
|
40
43
|
end
|
44
|
+
|
45
|
+
if @options['host']
|
46
|
+
@host = @options['host']
|
47
|
+
end
|
48
|
+
|
49
|
+
# WARNING: name should never be updated
|
50
|
+
@name = @options['name'] || @uri
|
41
51
|
end
|
42
52
|
|
43
53
|
def update_conf(conf)
|
@@ -48,6 +58,7 @@ module Bolt
|
|
48
58
|
@user = t_conf['user']
|
49
59
|
@password = t_conf['password']
|
50
60
|
@port = t_conf['port']
|
61
|
+
@host = t_conf['host']
|
51
62
|
|
52
63
|
# Preserve everything in options so we can easily create copies of a Target.
|
53
64
|
@options = t_conf.merge(@options)
|
@@ -56,7 +67,9 @@ module Bolt
|
|
56
67
|
end
|
57
68
|
|
58
69
|
def parse(string)
|
59
|
-
if string
|
70
|
+
if string.nil?
|
71
|
+
nil
|
72
|
+
elsif string =~ %r{^[^:]+://}
|
60
73
|
Addressable::URI.parse(string)
|
61
74
|
else
|
62
75
|
# Initialize with an empty scheme to ensure we parse the hostname correctly
|
@@ -75,8 +88,17 @@ module Bolt
|
|
75
88
|
end
|
76
89
|
end
|
77
90
|
|
91
|
+
# TODO: WHAT does equality mean here?
|
92
|
+
# should we just compare names? is there something else that is meaninful?
|
78
93
|
def eql?(other)
|
79
|
-
self.class.equal?(other.class)
|
94
|
+
if self.class.equal?(other.class)
|
95
|
+
if @uri
|
96
|
+
return @uri == other.uri
|
97
|
+
else
|
98
|
+
@name = other.name
|
99
|
+
end
|
100
|
+
end
|
101
|
+
false
|
80
102
|
end
|
81
103
|
alias == eql?
|
82
104
|
|
@@ -102,21 +124,19 @@ module Bolt
|
|
102
124
|
end
|
103
125
|
|
104
126
|
def host
|
105
|
-
@uri_obj
|
127
|
+
@uri_obj&.hostname || @host
|
106
128
|
end
|
107
129
|
|
108
|
-
# name is currently just uri but should be used instead to identify the
|
109
|
-
# Target ouside the transport or uri options.
|
110
130
|
def name
|
111
|
-
uri
|
131
|
+
@name || @uri
|
112
132
|
end
|
113
133
|
|
114
134
|
def remote?
|
115
|
-
@uri_obj
|
135
|
+
@uri_obj&.scheme == 'remote' || @protocol == 'remote'
|
116
136
|
end
|
117
137
|
|
118
138
|
def port
|
119
|
-
@uri_obj
|
139
|
+
@uri_obj&.port || @port
|
120
140
|
end
|
121
141
|
|
122
142
|
# transport is separate from protocol for remote targets.
|
@@ -125,15 +145,15 @@ module Bolt
|
|
125
145
|
end
|
126
146
|
|
127
147
|
def protocol
|
128
|
-
@uri_obj
|
148
|
+
@uri_obj&.scheme || @protocol
|
129
149
|
end
|
130
150
|
|
131
151
|
def user
|
132
|
-
Addressable::URI.unencode_component(@uri_obj
|
152
|
+
Addressable::URI.unencode_component(@uri_obj&.user) || @user
|
133
153
|
end
|
134
154
|
|
135
155
|
def password
|
136
|
-
Addressable::URI.unencode_component(@uri_obj
|
156
|
+
Addressable::URI.unencode_component(@uri_obj&.password) || @password
|
137
157
|
end
|
138
158
|
end
|
139
159
|
end
|