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.

Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/bolt-modules/boltlib/lib/puppet/datatypes/target.rb +1 -1
  3. data/bolt-modules/boltlib/lib/puppet/functions/run_plan.rb +4 -4
  4. data/lib/bolt.rb +3 -0
  5. data/lib/bolt/analytics.rb +7 -2
  6. data/lib/bolt/applicator.rb +6 -2
  7. data/lib/bolt/bolt_option_parser.rb +4 -4
  8. data/lib/bolt/cli.rb +8 -4
  9. data/lib/bolt/config.rb +6 -6
  10. data/lib/bolt/executor.rb +2 -7
  11. data/lib/bolt/inventory.rb +37 -6
  12. data/lib/bolt/inventory/group2.rb +314 -0
  13. data/lib/bolt/inventory/inventory2.rb +261 -0
  14. data/lib/bolt/outputter/human.rb +3 -1
  15. data/lib/bolt/pal.rb +8 -7
  16. data/lib/bolt/puppetdb/client.rb +6 -5
  17. data/lib/bolt/target.rb +34 -14
  18. data/lib/bolt/task.rb +2 -2
  19. data/lib/bolt/transport/base.rb +2 -2
  20. data/lib/bolt/transport/docker.rb +1 -1
  21. data/lib/bolt/transport/docker/connection.rb +2 -0
  22. data/lib/bolt/transport/local.rb +9 -181
  23. data/lib/bolt/transport/local/shell.rb +202 -12
  24. data/lib/bolt/transport/local_windows.rb +203 -0
  25. data/lib/bolt/transport/orch.rb +6 -4
  26. data/lib/bolt/transport/orch/connection.rb +6 -2
  27. data/lib/bolt/transport/ssh.rb +10 -150
  28. data/lib/bolt/transport/ssh/connection.rb +15 -116
  29. data/lib/bolt/transport/sudoable.rb +163 -0
  30. data/lib/bolt/transport/sudoable/connection.rb +76 -0
  31. data/lib/bolt/transport/sudoable/tmpdir.rb +59 -0
  32. data/lib/bolt/transport/winrm.rb +4 -4
  33. data/lib/bolt/transport/winrm/connection.rb +1 -0
  34. data/lib/bolt/util.rb +2 -0
  35. data/lib/bolt/version.rb +1 -1
  36. data/lib/bolt_ext/puppetdb_inventory.rb +0 -1
  37. data/lib/bolt_server/transport_app.rb +3 -1
  38. data/lib/logging_extensions/logging.rb +13 -0
  39. data/lib/plan_executor/orch_client.rb +4 -0
  40. 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
@@ -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 = Concurrent.processor_count)
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 => err
111
- err
112
- rescue Puppet::PreformattedError => err
113
- PALError.from_preformatted_error(err)
114
- rescue StandardError => err
115
- PALError.from_preformatted_error(err)
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
@@ -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 => err
54
- raise Bolt::PuppetDBFailoverError, "Failed to query PuppetDB: #{err}"
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 => err
72
- @logger.error("Request to puppetdb at #{@current_url} failed with #{err}.")
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 :uri, :options
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 =~ %r{^[^:]+://}
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) && @uri == other.uri
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.hostname
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.scheme == 'remote' || @protocol == 'remote'
135
+ @uri_obj&.scheme == 'remote' || @protocol == 'remote'
116
136
  end
117
137
 
118
138
  def port
119
- @uri_obj.port || @port
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.scheme || @protocol
148
+ @uri_obj&.scheme || @protocol
129
149
  end
130
150
 
131
151
  def user
132
- Addressable::URI.unencode_component(@uri_obj.user) || @user
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.password) || @password
156
+ Addressable::URI.unencode_component(@uri_obj&.password) || @password
137
157
  end
138
158
  end
139
159
  end