puppet_litmus 0.18.2 → 0.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c9148f5cf259702d6b12f875555b4cbef8fdbc4ff78081e495ca1b9b8948d082
4
- data.tar.gz: 3b002919bfff6ce6cb794ed9bd3d8b6b5712ba76d1cceceba1c8d205488f9057
3
+ metadata.gz: f31e0247c29a20874c704110cac6c6f9c9724624a3fc4bbe1dafd872eee85131
4
+ data.tar.gz: b940b92982bd7d55861d963ee99f3bc653888063f5d1a304686b276350b18e0e
5
5
  SHA512:
6
- metadata.gz: 458f9935cf9148d839f0a2ac3cb3851a85ef18df7a7e99a0b216adafb9d33999a1d672da95f5450c1b371794cd6ecf7d92131778d79cdb47ea0cd0788e1901c4
7
- data.tar.gz: 4c1c40a1b40865ff788af2f52c6766a5f5d159674ce3f62173da91713dd96f5e1e78f1fb41234e5a7d82aa89c1cb1b2f35a29c841bf2e3307719506c4a130793
6
+ metadata.gz: 7c0272e6b4f6b840b1a294832fa4a5ec8e7b1159f3c08105fbc490e40dbc7535c1297d0f544ac19ce7730b78232944edc06951c0eb3393d39d5fc66b5f49198e
7
+ data.tar.gz: 9a29fff33eddf104445edbf5b9c7e2ce31f0a67d9da9d9e4d52eb32699f822062e9dee75a2d7c52aecb7bd2a1de6d6aff866708b5dd3d719b96df0af00906b88
data/README.md CHANGED
@@ -24,7 +24,7 @@ Install Litmus as a gem by running ```gem install puppet_litmus```.
24
24
 
25
25
  ## Documentation
26
26
 
27
- For documentation, see our [Litmus Wiki](https://github.com/puppetlabs/puppet_litmus/wiki).
27
+ For documentation, see our [Litmus Docs Site](https://puppetlabs.github.io/litmus/).
28
28
 
29
29
  ## Other Resources
30
30
 
@@ -0,0 +1,99 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # this script creates a build matrix for github actions from the claimed supported platforms and puppet versions in metadata.json
5
+
6
+ require 'json'
7
+
8
+ IMAGE_TABLE = {
9
+ 'RedHat-6' => 'rhel-6',
10
+ 'RedHat-7' => 'rhel-7',
11
+ 'RedHat-8' => 'rhel-8',
12
+ 'SLES-12' => 'sles-12',
13
+ 'SLES-15' => 'sles-15',
14
+ 'Windows-2012 R2' => 'windows-2012-r2-core',
15
+ 'Windows-2016' => 'windows-2016',
16
+ 'Windows-2019' => 'windows-2019-core',
17
+ }.freeze
18
+
19
+ DOCKER_PLATFORMS = [
20
+ 'CentOS-6',
21
+ 'CentOS-7',
22
+ 'CentOS-8',
23
+ 'Debian-10',
24
+ 'Debian-8',
25
+ 'Debian-9',
26
+ 'OracleLinux-6',
27
+ 'OracleLinux-7',
28
+ 'Scientific-6',
29
+ 'Scientific-7',
30
+ 'Ubuntu-14.04',
31
+ 'Ubuntu-16.04',
32
+ 'Ubuntu-18.04',
33
+ 'Ubuntu-20.04',
34
+ ].freeze
35
+
36
+ # This table uses the latest version in each collection for accurate
37
+ # comparison when evaluating puppet requirements from the metadata
38
+ COLLECTION_TABLE = {
39
+ '5.5.22' => 'puppet5',
40
+ '6.19.1' => 'puppet6',
41
+ '7.0.0' => 'puppet7-nightly',
42
+ }.freeze
43
+
44
+ matrix = {
45
+ platform: [],
46
+ collection: [],
47
+ }
48
+
49
+ metadata = JSON.parse(File.read('metadata.json'))
50
+ # Set platforms based on declared operating system support
51
+ metadata['operatingsystem_support'].sort_by { |a| a['operatingsystem'] }.each do |sup|
52
+ os = sup['operatingsystem']
53
+ sup['operatingsystemrelease'].sort_by { |a| a.to_i }.each do |ver|
54
+ image_key = "#{os}-#{ver}"
55
+ if IMAGE_TABLE.key? image_key
56
+ matrix[:platform] << IMAGE_TABLE[image_key]
57
+ elsif DOCKER_PLATFORMS.include? image_key
58
+ puts "Expecting #{image_key} test using docker on travis"
59
+ else
60
+ puts "::warning::Cannot find image for #{image_key}"
61
+ end
62
+ end
63
+ end
64
+
65
+ # Set collections based on puppet version requirements
66
+ if metadata.key?('requirements') && metadata['requirements'].length.positive?
67
+ metadata['requirements'].each do |req|
68
+ next unless req.key?('name') && req.key?('version_requirement') && req['name'] == 'puppet'
69
+
70
+ ver_regexp = %r{^([>=<]{1,2})\s*([\d.]+)\s+([>=<]{1,2})\s*([\d.]+)$}
71
+ match = ver_regexp.match(req['version_requirement'])
72
+ if match.nil?
73
+ puts "::warning::Didn't recognize version_requirement '#{req['version_requirement']}'"
74
+ break
75
+ end
76
+
77
+ cmp_one, ver_one, cmp_two, ver_two = match.captures
78
+ reqs = ["#{cmp_one} #{ver_one}", "#{cmp_two} #{ver_two}"]
79
+
80
+ COLLECTION_TABLE.each do |key, val|
81
+ if Gem::Requirement.create(reqs).satisfied_by?(Gem::Version.new(key))
82
+ matrix[:collection] << val
83
+ end
84
+ end
85
+ end
86
+ end
87
+
88
+ # Set to defaults (all collections) if no matches are found
89
+ if matrix[:collection].empty?
90
+ matrix[:collection] = COLLECTION_TABLE.values
91
+ end
92
+
93
+ # Just to make sure there aren't any duplicates
94
+ matrix[:platform] = matrix[:platform].uniq.sort
95
+ matrix[:collection] = matrix[:collection].uniq.sort
96
+
97
+ puts "::set-output name=matrix::#{JSON.generate(matrix)}"
98
+
99
+ puts "Created matrix with #{matrix[:platform].length * matrix[:collection].length} cells."
@@ -18,7 +18,7 @@ module PuppetLitmus::InventoryManipulation
18
18
  raise "There is no inventory file at '#{inventory_full_path}'." unless File.exist?(inventory_full_path)
19
19
 
20
20
  inventory_hash = YAML.load_file(inventory_full_path)
21
- raise "Inventory file is incompatible (version 2 and up). Try the 'bolt project migrate' command." if inventory_hash.dig('version').nil? || (inventory_hash['version'] < 2)
21
+ raise "Inventory file is incompatible (version 2 and up). Try the 'bolt project migrate' command." if inventory_hash['version'].nil? || (inventory_hash['version'] < 2)
22
22
 
23
23
  inventory_hash
24
24
  end
@@ -50,12 +50,11 @@ module PuppetLitmus::InventoryManipulation
50
50
  # @param targets [Array]
51
51
  # @return [Array] array of targets.
52
52
  def find_targets(inventory_hash, targets)
53
- targets = if targets.nil?
54
- inventory_hash.to_s.scan(%r{uri"=>"(\S*)"}).flatten
55
- else
56
- [targets]
57
- end
58
- targets
53
+ if targets.nil?
54
+ inventory_hash.to_s.scan(%r{uri"=>"(\S*)"}).flatten
55
+ else
56
+ [targets]
57
+ end
59
58
  end
60
59
 
61
60
  # Determines if a node_name exists in a group in the inventory_hash.
@@ -263,7 +262,12 @@ module PuppetLitmus::InventoryManipulation
263
262
  # @param inventory_hash [Hash] hash of the inventory.yaml file
264
263
  # @param node_name [String] node of nodes to limit the search for the node_name in
265
264
  def add_platform_field(inventory_hash, node_name)
266
- facts = facts_from_node(inventory_hash, node_name)
265
+ facts = begin
266
+ facts_from_node(inventory_hash, node_name)
267
+ rescue StandardError => e
268
+ warn e
269
+ {}
270
+ end
267
271
  Honeycomb.current_span.add_field('litmus.platform', facts&.dig('platform'))
268
272
  end
269
273
  end
@@ -9,7 +9,7 @@ module PuppetLitmus::PuppetHelpers
9
9
  # @return [Boolean] The result of the 2 apply manifests.
10
10
  def idempotent_apply(manifest)
11
11
  Honeycomb.start_span(name: 'litmus.idempotent_apply') do |span|
12
- ENV['HTTP_X_HONEYCOMB_TRACE'] = span.to_trace_header unless ENV['HTTP_X_HONEYCOMB_TRACE']
12
+ ENV['HONEYCOMB_TRACE'] = span.to_trace_header
13
13
  manifest_file_location = create_manifest_file(manifest)
14
14
  apply_manifest(nil, expect_failures: false, manifest_file_location: manifest_file_location)
15
15
  apply_manifest(nil, catch_changes: true, manifest_file_location: manifest_file_location)
@@ -39,7 +39,7 @@ module PuppetLitmus::PuppetHelpers
39
39
  # @return [Object] A result object from the apply.
40
40
  def apply_manifest(manifest, opts = {})
41
41
  Honeycomb.start_span(name: 'litmus.apply_manifest') do |span|
42
- ENV['HTTP_X_HONEYCOMB_TRACE'] = span.to_trace_header unless ENV['HTTP_X_HONEYCOMB_TRACE']
42
+ ENV['HONEYCOMB_TRACE'] = span.to_trace_header
43
43
  span.add_field('litmus.manifest', manifest)
44
44
  span.add_field('litmus.opts', opts)
45
45
 
@@ -117,7 +117,7 @@ module PuppetLitmus::PuppetHelpers
117
117
  # @return [String] The path to the location of the manifest.
118
118
  def create_manifest_file(manifest)
119
119
  Honeycomb.start_span(name: 'litmus.create_manifest_file') do |span|
120
- ENV['HTTP_X_HONEYCOMB_TRACE'] = span.to_trace_header unless ENV['HTTP_X_HONEYCOMB_TRACE']
120
+ ENV['HONEYCOMB_TRACE'] = span.to_trace_header
121
121
  span.add_field('litmus.manifest', manifest)
122
122
 
123
123
  require 'tmpdir'
@@ -147,6 +147,42 @@ module PuppetLitmus::PuppetHelpers
147
147
  end
148
148
  end
149
149
 
150
+ # Writes a string variable to a file on a target node at a specified path.
151
+ #
152
+ # @param content [String] String data to write to the file.
153
+ # @param destination [String] The path on the target node to write the file.
154
+ # @return [Bool] Success. The file was succesfully writtne on the target.
155
+ def write_file(content, destination)
156
+ Honeycomb.start_span(name: 'litmus.write_file') do |span|
157
+ ENV['HONEYCOMB_TRACE'] = span.to_trace_header
158
+ span.add_field('litmus.destination', destination)
159
+
160
+ require 'tmpdir'
161
+ target_node_name = ENV['TARGET_HOST']
162
+
163
+ Tempfile.create('litmus') do |tmp_file|
164
+ tmp_file.write(content)
165
+ tmp_file.flush
166
+ if target_node_name.nil? || target_node_name == 'localhost'
167
+ require 'fileutils'
168
+ # no need to transfer
169
+ FileUtils.cp(tmp_file.path, destination)
170
+ else
171
+ # transfer to TARGET_HOST
172
+ inventory_hash = inventory_hash_from_inventory_file
173
+ span.add_field('litmus.node_name', target_node_name)
174
+ add_platform_field(inventory_hash, target_node_name)
175
+
176
+ bolt_result = upload_file(tmp_file.path, destination, target_node_name, options: {}, config: nil, inventory: inventory_hash)
177
+ span.add_field('litmus.bolt_result.file_upload', bolt_result)
178
+ raise bolt_result.first['value'].to_s unless bolt_result.first['status'] == 'success'
179
+ end
180
+ end
181
+
182
+ true
183
+ end
184
+ end
185
+
150
186
  # Runs a command against the target system
151
187
  #
152
188
  # @param command_to_run [String] The command to execute.
@@ -155,7 +191,7 @@ module PuppetLitmus::PuppetHelpers
155
191
  # @return [Object] A result object from the command.
156
192
  def run_shell(command_to_run, opts = {})
157
193
  Honeycomb.start_span(name: 'litmus.run_shell') do |span|
158
- ENV['HTTP_X_HONEYCOMB_TRACE'] = span.to_trace_header unless ENV['HTTP_X_HONEYCOMB_TRACE']
194
+ ENV['HONEYCOMB_TRACE'] = span.to_trace_header
159
195
  span.add_field('litmus.command_to_run', command_to_run)
160
196
  span.add_field('litmus.opts', opts)
161
197
 
@@ -192,7 +228,7 @@ module PuppetLitmus::PuppetHelpers
192
228
  # @return [Object] A result object from the command.
193
229
  def bolt_upload_file(source, destination, opts = {}, options = {})
194
230
  Honeycomb.start_span(name: 'litmus.bolt_upload_file') do |span|
195
- ENV['HTTP_X_HONEYCOMB_TRACE'] = span.to_trace_header unless ENV['HTTP_X_HONEYCOMB_TRACE']
231
+ ENV['HONEYCOMB_TRACE'] = span.to_trace_header
196
232
  span.add_field('litmus.source', source)
197
233
  span.add_field('litmus.destination', destination)
198
234
  span.add_field('litmus.opts', opts)
@@ -244,7 +280,7 @@ module PuppetLitmus::PuppetHelpers
244
280
  # @return [Object] A result object from the task.The values available are stdout, stderr and result.
245
281
  def run_bolt_task(task_name, params = {}, opts = {})
246
282
  Honeycomb.start_span(name: 'litmus.run_task') do |span|
247
- ENV['HTTP_X_HONEYCOMB_TRACE'] = span.to_trace_header unless ENV['HTTP_X_HONEYCOMB_TRACE']
283
+ ENV['HONEYCOMB_TRACE'] = span.to_trace_header
248
284
  span.add_field('litmus.task_name', task_name)
249
285
  span.add_field('litmus.params', params)
250
286
  span.add_field('litmus.opts', opts)
@@ -312,7 +348,7 @@ module PuppetLitmus::PuppetHelpers
312
348
  # @return [Object] A result object from the script run.
313
349
  def bolt_run_script(script, opts = {}, arguments: [])
314
350
  Honeycomb.start_span(name: 'litmus.bolt_run_script') do |span|
315
- ENV['HTTP_X_HONEYCOMB_TRACE'] = span.to_trace_header unless ENV['HTTP_X_HONEYCOMB_TRACE']
351
+ ENV['HONEYCOMB_TRACE'] = span.to_trace_header
316
352
  span.add_field('litmus.script', script)
317
353
  span.add_field('litmus.opts', opts)
318
354
  span.add_field('litmus.arguments', arguments)
@@ -382,7 +418,7 @@ module PuppetLitmus::PuppetHelpers
382
418
 
383
419
  # Return the stdout of the puppet run
384
420
  def puppet_output(bolt_result)
385
- bolt_result.dig(0, 'value', 'stderr').to_s << \
421
+ bolt_result.dig(0, 'value', 'stderr').to_s + \
386
422
  bolt_result.dig(0, 'value', 'stdout').to_s
387
423
  end
388
424
 
@@ -2,15 +2,21 @@
2
2
 
3
3
  require 'bolt_spec/run'
4
4
  require 'honeycomb-beeline'
5
+ require 'puppet_litmus/version'
5
6
  Honeycomb.configure do |config|
6
7
  # override client if no configuration is provided, so that the pesky libhoney warning about lack of configuration is not shown
7
8
  unless ENV['HONEYCOMB_WRITEKEY'] && ENV['HONEYCOMB_DATASET']
8
9
  config.client = Libhoney::NullClient.new
9
10
  end
10
11
  end
11
- process_span = Honeycomb.start_span(name: 'Litmus Testing', serialized_trace: ENV['HTTP_X_HONEYCOMB_TRACE'])
12
- ENV['HTTP_X_HONEYCOMB_TRACE'] = process_span.to_trace_header unless ENV['HTTP_X_HONEYCOMB_TRACE']
12
+ process_span = Honeycomb.start_span(name: "litmus: #{([$PROGRAM_NAME] + ($ARGV || [])).join(' ')}", serialized_trace: ENV['HONEYCOMB_TRACE'])
13
+ ENV['HONEYCOMB_TRACE'] = process_span.to_trace_header
13
14
  Honeycomb.add_field_to_trace('litmus.pid', Process.pid)
15
+ if defined? PuppetLitmus::VERSION
16
+ Honeycomb.add_field_to_trace('litmus.version', PuppetLitmus::VERSION)
17
+ else
18
+ Honeycomb.add_field_to_trace('litmus.version', 'undefined')
19
+ end
14
20
  if ENV['CI'] == 'true' && ENV['TRAVIS'] == 'true'
15
21
  Honeycomb.add_field_to_trace('module_name', ENV['TRAVIS_REPO_SLUG'])
16
22
  Honeycomb.add_field_to_trace('ci.provider', 'travis')
@@ -35,6 +41,13 @@ elsif ENV['GITHUB_ACTIONS'] == 'true'
35
41
  Honeycomb.add_field_to_trace('ci.sha', ENV['GITHUB_SHA'])
36
42
  end
37
43
  at_exit do
44
+ if $ERROR_INFO.is_a?(SystemExit)
45
+ process_span.add_field('process.exit_code', $ERROR_INFO.status)
46
+ elsif $ERROR_INFO
47
+ process_span.add_field('process.exit_code', $ERROR_INFO.class.name)
48
+ else
49
+ process_span.add_field('process.exit_code', 'unknown')
50
+ end
38
51
  process_span.send
39
52
  end
40
53
 
@@ -42,7 +55,7 @@ end
42
55
  module PuppetLitmus::RakeHelper
43
56
  # DEFAULT_CONFIG_DATA should be frozen for our safety, but it needs to work around https://github.com/puppetlabs/bolt/pull/1696
44
57
  DEFAULT_CONFIG_DATA ||= { 'modulepath' => File.join(Dir.pwd, 'spec', 'fixtures', 'modules') } # .freeze # rubocop:disable Style/MutableConstant
45
- SUPPORTED_PROVISIONERS ||= %w[abs docker docker_exp vagrant vmpooler].freeze
58
+ SUPPORTED_PROVISIONERS ||= %w[abs docker docker_exp provision_service vagrant vmpooler].freeze
46
59
 
47
60
  # Gets a string representing the operating system and version.
48
61
  #
@@ -90,7 +103,7 @@ module PuppetLitmus::RakeHelper
90
103
  # @return [Object] the standard out stream.
91
104
  def run_local_command(command)
92
105
  Honeycomb.start_span(name: 'litmus.run_local_command') do |span|
93
- ENV['HTTP_X_HONEYCOMB_TRACE'] = span.to_trace_header unless ENV['HTTP_X_HONEYCOMB_TRACE']
106
+ ENV['HONEYCOMB_TRACE'] = span.to_trace_header
94
107
  span.add_field('litmus.command', command)
95
108
 
96
109
  require 'open3'
@@ -113,13 +126,18 @@ module PuppetLitmus::RakeHelper
113
126
 
114
127
  Honeycomb.add_field_to_trace('litmus.provisioner', provisioner)
115
128
  Honeycomb.start_span(name: 'litmus.provision') do |span|
116
- ENV['HTTP_X_HONEYCOMB_TRACE'] = span.to_trace_header unless ENV['HTTP_X_HONEYCOMB_TRACE']
129
+ ENV['HONEYCOMB_TRACE'] = span.to_trace_header
117
130
  span.add_field('litmus.platform', platform)
118
- span.add_field('litmus.inventory', params['inventory'])
131
+
132
+ task_name = provisioner_task(provisioner)
133
+ span.add_field('litmus.task_name', task_name)
134
+ span.add_field('litmus.params', params)
119
135
  span.add_field('litmus.config', DEFAULT_CONFIG_DATA)
120
136
 
121
- bolt_result = run_task(provisioner_task(provisioner), 'localhost', params, config: DEFAULT_CONFIG_DATA, inventory: nil)
137
+ bolt_result = run_task(task_name, 'localhost', params, config: DEFAULT_CONFIG_DATA, inventory: nil)
138
+ span.add_field('litmus.result', bolt_result)
122
139
  span.add_field('litmus.node_name', bolt_result&.first&.dig('value', 'node_name'))
140
+
123
141
  raise_bolt_errors(bolt_result, "provisioning of #{platform} failed.")
124
142
 
125
143
  bolt_result
@@ -142,7 +160,7 @@ module PuppetLitmus::RakeHelper
142
160
 
143
161
  def tear_down_nodes(targets, inventory_hash)
144
162
  Honeycomb.start_span(name: 'litmus.tear_down_nodes') do |span|
145
- ENV['HTTP_X_HONEYCOMB_TRACE'] = span.to_trace_header unless ENV['HTTP_X_HONEYCOMB_TRACE']
163
+ ENV['HONEYCOMB_TRACE'] = span.to_trace_header
146
164
  span.add_field('litmus.targets', targets)
147
165
 
148
166
  include ::BoltSpec::Run
@@ -154,6 +172,18 @@ module PuppetLitmus::RakeHelper
154
172
  next if node_name == 'litmus_localhost'
155
173
 
156
174
  result = tear_down(node_name, inventory_hash)
175
+ # Some provisioners tear_down targets that were created as a batch job.
176
+ # These provisioners should return the list of additional targets
177
+ # removed so that we do not attempt to process them.
178
+ if result != [] && result[0]['value'].key?('removed')
179
+ removed_targets = result[0]['value']['removed']
180
+ result[0]['value'].delete('removed')
181
+ removed_targets.each do |removed_target|
182
+ targets.delete(removed_target)
183
+ results[removed_target] = result
184
+ end
185
+ end
186
+
157
187
  results[node_name] = result unless result == []
158
188
  end
159
189
  results
@@ -162,7 +192,7 @@ module PuppetLitmus::RakeHelper
162
192
 
163
193
  def tear_down(node_name, inventory_hash)
164
194
  Honeycomb.start_span(name: 'litmus.tear_down') do |span|
165
- ENV['HTTP_X_HONEYCOMB_TRACE'] = span.to_trace_header unless ENV['HTTP_X_HONEYCOMB_TRACE']
195
+ ENV['HONEYCOMB_TRACE'] = span.to_trace_header
166
196
  # how do we know what provisioner to use
167
197
 
168
198
  span.add_field('litmus.node_name', node_name)
@@ -178,7 +208,7 @@ module PuppetLitmus::RakeHelper
178
208
 
179
209
  def install_agent(collection, targets, inventory_hash)
180
210
  Honeycomb.start_span(name: 'litmus.install_agent') do |span|
181
- ENV['HTTP_X_HONEYCOMB_TRACE'] = span.to_trace_header unless ENV['HTTP_X_HONEYCOMB_TRACE']
211
+ ENV['HONEYCOMB_TRACE'] = span.to_trace_header
182
212
  span.add_field('litmus.collection', collection)
183
213
  span.add_field('litmus.targets', targets)
184
214
 
@@ -186,7 +216,6 @@ module PuppetLitmus::RakeHelper
186
216
  params = if collection.nil?
187
217
  {}
188
218
  else
189
- Honeycomb.current_span.add_field('litmus.collection', collection)
190
219
  { 'collection' => collection }
191
220
  end
192
221
  raise "puppet_agent was not found in #{DEFAULT_CONFIG_DATA['modulepath']}, please amend the .fixtures.yml file" \
@@ -202,39 +231,83 @@ module PuppetLitmus::RakeHelper
202
231
  def configure_path(inventory_hash)
203
232
  results = []
204
233
  # fix the path on ssh_nodes
205
- unless inventory_hash['groups'].select { |group| group['name'] == 'ssh_nodes' }.size.zero?
206
- results = run_command('echo PATH="$PATH:/opt/puppetlabs/puppet/bin" > /etc/environment',
207
- 'ssh_nodes', config: nil, inventory: inventory_hash)
234
+ unless inventory_hash['groups'].select { |group| group['name'] == 'ssh_nodes' && !group['targets'].empty? }.size.zero?
235
+ results << run_command('echo PATH="$PATH:/opt/puppetlabs/puppet/bin" > /etc/environment',
236
+ 'ssh_nodes', config: nil, inventory: inventory_hash)
237
+ end
238
+ unless inventory_hash['groups'].select { |group| group['name'] == 'winrm_nodes' && !group['targets'].empty? }.size.zero?
239
+ results << run_command('[Environment]::SetEnvironmentVariable("Path", $env:Path + ";C:\Program Files\Puppet Labs\Puppet\bin;C:\Program Files (x86)\Puppet Labs\Puppet\bin", "Machine")',
240
+ 'winrm_nodes', config: nil, inventory: inventory_hash)
208
241
  end
209
242
  results
210
243
  end
211
244
 
245
+ # Build the module in `module_dir` and put the resulting compressed tarball into `target_dir`.
246
+ #
212
247
  # @param opts Hash of options to build the module
213
248
  # @param module_dir [String] The path of the module to build. If missing defaults to Dir.pwd
214
- # @param target_dir [String] The path the module will be built into. The default is <source_dir>/pkg
249
+ # @param target_dir [String] The path the module will be built into. The default is <module_dir>/pkg
215
250
  # @return [String] The path to the built module
216
251
  def build_module(module_dir = nil, target_dir = nil)
217
252
  require 'puppet/modulebuilder'
218
253
 
219
- source_dir = module_dir || Dir.pwd
220
- dest_dir = target_dir || File.join(source_dir, 'pkg')
254
+ module_dir ||= Dir.pwd
255
+ target_dir ||= File.join(source_dir, 'pkg')
256
+
257
+ puts "Building '#{module_dir}' into '#{target_dir}'"
258
+ builder = Puppet::Modulebuilder::Builder.new(module_dir, target_dir, nil)
221
259
 
222
- builder = Puppet::Modulebuilder::Builder.new(source_dir, dest_dir, nil)
223
260
  # Force the metadata to be read. Raises if metadata could not be found
224
261
  _metadata = builder.metadata
225
262
 
226
263
  builder.build
227
264
  end
228
265
 
229
- def install_module(inventory_hash, target_node_name, module_tar, module_repository = 'https://forgeapi.puppetlabs.com')
266
+ # Builds all the modules in a specified directory
267
+ #
268
+ # @param source_dir [String] the directory to get the modules from
269
+ # @param target_dir [String] temporary location to store tarballs before uploading. This directory will be cleaned before use. The default is <source_dir>/pkg
270
+ # @return [Array] an array of module tars' filenames
271
+ def build_modules_in_dir(source_dir, target_dir = nil)
272
+ target_dir ||= File.join(Dir.pwd, 'pkg')
273
+ # remove old build dir if exists, before we build afresh
274
+ FileUtils.rm_rf(target_dir) if File.directory?(target_dir)
275
+
276
+ module_tars = Dir.entries(source_dir).map do |entry|
277
+ next if ['.', '..'].include? entry
278
+
279
+ module_dir = File.join(source_dir, entry)
280
+ next unless File.directory? module_dir
281
+
282
+ build_module(module_dir, target_dir)
283
+ end
284
+ module_tars.compact
285
+ end
286
+
287
+ # @deprecated Use `build_modules_in_dir` instead
288
+ def build_modules_in_folder(source_folder)
289
+ build_modules_in_dir(source_folder)
290
+ end
291
+
292
+ # Install a specific module tarball to the specified target.
293
+ # This method installs dependencies using a forge repository.
294
+ #
295
+ # @param inventory_hash [Hash] the pre-loaded inventory
296
+ # @param target_node_name [String] the name of the target where the module should be installed
297
+ # @param module_tar [String] the filename of the module tarball to upload
298
+ # @param module_repository [String] the URL for the forge to use for downloading modules. Defaults to the public Forge API.
299
+ # @param ignore_dependencies [Boolean] flag used to ignore module dependencies defaults to false.
300
+ # @return a bolt result
301
+ def install_module(inventory_hash, target_node_name, module_tar, module_repository = nil, ignore_dependencies = false) # rubocop:disable Style/OptionalBooleanParameter
230
302
  Honeycomb.start_span(name: 'install_module') do |span|
231
- ENV['HTTP_X_HONEYCOMB_TRACE'] = span.to_trace_header unless ENV['HTTP_X_HONEYCOMB_TRACE']
303
+ ENV['HONEYCOMB_TRACE'] = span.to_trace_header
232
304
  span.add_field('litmus.target_node_name', target_node_name)
233
305
  span.add_field('litmus.module_tar', module_tar)
234
306
 
235
- # make sure the target module is not installed
307
+ # make sure the module to install is not installed
236
308
  # otherwise `puppet module install` might silently skip it
237
- uninstall_module(inventory_hash.clone, target_node_name, force: true)
309
+ module_name = File.basename(module_tar, '.tar.gz').split('-', 3)[0..1].join('-')
310
+ uninstall_module(inventory_hash.clone, target_node_name, module_name, force: true)
238
311
 
239
312
  include ::BoltSpec::Run
240
313
 
@@ -243,7 +316,9 @@ module PuppetLitmus::RakeHelper
243
316
  bolt_result = upload_file(module_tar, File.basename(module_tar), target_nodes, options: {}, config: nil, inventory: inventory_hash.clone)
244
317
  raise_bolt_errors(bolt_result, 'Failed to upload module.')
245
318
 
246
- install_module_command = "puppet module install --module_repository '#{module_repository}' #{File.basename(module_tar)}"
319
+ module_repository_opts = "--module_repository '#{module_repository}'" unless module_repository.nil?
320
+ install_module_command = "puppet module install #{module_repository_opts} #{File.basename(module_tar)}"
321
+ install_module_command += ' --ignore-dependencies --force' if ignore_dependencies.to_s.downcase == 'true'
247
322
  span.add_field('litmus.install_module_command', install_module_command)
248
323
 
249
324
  bolt_result = run_command(install_module_command, target_nodes, config: nil, inventory: inventory_hash.clone)
@@ -252,31 +327,6 @@ module PuppetLitmus::RakeHelper
252
327
  end
253
328
  end
254
329
 
255
- # Builds all the modules in a specified module
256
- #
257
- # @param source_folder [String] the folder to get the modules from
258
- # @return [Array] an array of module tar's
259
- def build_modules_in_folder(source_folder)
260
- folder_list = Dir.entries(source_folder).reject { |f| File.directory? f }
261
- module_tars = []
262
-
263
- target_dir = File.join(Dir.pwd, 'pkg')
264
- # remove old build folder if exists, before we build afresh
265
- FileUtils.rm_rf(target_dir) if File.directory?(target_dir)
266
-
267
- folder_list.each do |folder|
268
- folder_handle = Dir.open(File.join(source_folder, folder))
269
- next if File.symlink?(folder_handle)
270
-
271
- module_dir = folder_handle.path
272
-
273
- # build_module
274
- module_tar = build_module(module_dir, target_dir)
275
- module_tars.push(File.new(module_tar))
276
- end
277
- module_tars
278
- end
279
-
280
330
  def metadata_module_name
281
331
  require 'json'
282
332
  raise 'Could not find metadata.json' unless File.exist?(File.join(Dir.pwd, 'metadata.json'))
@@ -287,6 +337,11 @@ module PuppetLitmus::RakeHelper
287
337
  metadata['name']
288
338
  end
289
339
 
340
+ # Uninstall a module from a specified target
341
+ # @param inventory_hash [Hash] the pre-loaded inventory
342
+ # @param target_node_name [String] the name of the target where the module should be uninstalled
343
+ # @param module_to_remove [String] the name of the module to remove. Defaults to the module under test.
344
+ # @param opts [Hash] additional options to pass on to `puppet module uninstall`
290
345
  def uninstall_module(inventory_hash, target_node_name, module_to_remove = nil, **opts)
291
346
  include ::BoltSpec::Run
292
347
  module_name = module_to_remove || metadata_module_name
@@ -301,24 +356,30 @@ module PuppetLitmus::RakeHelper
301
356
 
302
357
  def check_connectivity?(inventory_hash, target_node_name)
303
358
  Honeycomb.start_span(name: 'litmus.check_connectivity') do |span|
304
- ENV['HTTP_X_HONEYCOMB_TRACE'] = span.to_trace_header unless ENV['HTTP_X_HONEYCOMB_TRACE']
359
+ ENV['HONEYCOMB_TRACE'] = span.to_trace_header
305
360
  # if we're only checking connectivity for a single node
306
361
  if target_node_name
307
- span.add_field('litmus.node_name', target_node_name)
362
+ span.add_field('litmus.target_node_name', target_node_name)
308
363
  add_platform_field(inventory_hash, target_node_name)
309
364
  end
310
365
 
311
366
  include ::BoltSpec::Run
312
367
  target_nodes = find_targets(inventory_hash, target_node_name)
368
+ puts "Checking connectivity for #{target_nodes.inspect}"
369
+ span.add_field('litmus.target_nodes', target_nodes)
370
+
313
371
  results = run_command('cd .', target_nodes, config: nil, inventory: inventory_hash)
314
372
  span.add_field('litmus.bolt_result', results)
315
373
  failed = []
316
- results.each do |result|
317
- failed.push(result['target']) if result['status'] == 'failure'
374
+ results.reject { |r| r['status'] == 'success' }.each do |result|
375
+ puts "Failure connecting to #{result['target']}:\n#{result.inspect}"
376
+ failed.push(result['target'])
318
377
  end
319
- span.add_field('litmus.connectivity_failed', failed)
378
+ span.add_field('litmus.connectivity_success', results.select { |r| r['status'] == 'success' })
379
+ span.add_field('litmus.connectivity_failure', results.reject { |r| r['status'] == 'success' })
320
380
  raise "Connectivity has failed on: #{failed}" unless failed.length.zero?
321
381
 
382
+ puts 'Connectivity check PASSED.'
322
383
  true
323
384
  end
324
385
  end
@@ -346,7 +407,7 @@ module PuppetLitmus::RakeHelper
346
407
 
347
408
  target = target_result['target']
348
409
  # get some info from error
349
- errors[target] = target_result['value']['_error']
410
+ errors[target] = target_result['value']
350
411
  end
351
412
  errors
352
413
  end
@@ -359,9 +420,56 @@ module PuppetLitmus::RakeHelper
359
420
  errors = check_bolt_errors(result_set)
360
421
 
361
422
  unless errors.empty?
362
- raise "#{error_msg}\nErrors: #{errors}"
423
+ formatted_results = errors.map { |k, v| " #{k}: #{v.inspect}" }.join("\n")
424
+ raise "#{error_msg}\nResults:\n#{formatted_results}}"
363
425
  end
364
426
 
365
427
  nil
366
428
  end
429
+
430
+ def start_spinner(message)
431
+ if (ENV['CI'] || '').downcase == 'true'
432
+ puts message
433
+ spinner = Thread.new do
434
+ # CI systems are strange beasts, we only output a '.' every wee while to keep the terminal alive.
435
+ loop do
436
+ printf '.'
437
+ sleep(10)
438
+ end
439
+ end
440
+ else
441
+ require 'tty-spinner'
442
+ spinner = TTY::Spinner.new("[:spinner] #{message}")
443
+ spinner.auto_spin
444
+ end
445
+ spinner
446
+ end
447
+
448
+ def stop_spinner(spinner)
449
+ if (ENV['CI'] || '').downcase == 'true'
450
+ Thread.kill(spinner)
451
+ else
452
+ spinner.success
453
+ end
454
+ end
455
+
456
+ require 'retryable'
457
+
458
+ Retryable.configure do |config|
459
+ config.sleep = ->(n) { (1.5**n) + Random.rand(0.5) }
460
+ # config.log_method = ->(retries, exception) do
461
+ # Logger.new($stdout).debug("[Attempt ##{retries}] Retrying because [#{exception.class} - #{exception.message}]: #{exception.backtrace.first(5).join(' | ')}")
462
+ # end
463
+ end
464
+
465
+ class LitmusTimeoutError < StandardError; end
466
+
467
+ def with_retries(options: { tries: Float::INFINITY }, max_wait_minutes: 5)
468
+ stop = Time.now + (max_wait_minutes * 60)
469
+ Retryable.retryable(options.merge(not: [LitmusTimeoutError])) do
470
+ raise LitmusTimeoutError if Time.now > stop
471
+
472
+ yield
473
+ end
474
+ end
367
475
  end