puppet_litmus 0.18.1 → 0.20.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: 6b3fcb695b5204b13b091211f12201ade70cd34af04ad8aefd81856f5b212116
4
- data.tar.gz: 4f941ab55af8dbed7848e39fbdbeba76586aabce36a1a52194d76cee6680ecf3
3
+ metadata.gz: 6f012bba513b46a7010b134d2ca78b733c9bdad8320e13798a4439ae1a7b43e3
4
+ data.tar.gz: da8f952b4faf060ffe4ec4913235e065a17019ede06437dddb0077a123d61149
5
5
  SHA512:
6
- metadata.gz: 8254a6ec0ffe6a9d398730de3a4c5878330ece15912ebc39d560bb3e811616e05319f4577434d457088e44f48391e043579f808ec5d4628e2ed0bd49843bbf70
7
- data.tar.gz: cb009e0328b4373a3fccb987a02c143f0b61bd5ae6c3718ee9adb6553242b057a0674dc84cb13f6aa0f31821a130aeec1db8498a8957dbe5ba8eb2a2263332b5
6
+ metadata.gz: 1889d8192777dfb2e2a006c96c549e58b6c080d59a8c0279bf66ce07d048ebb03e5ce4cc694bb24e20ad37560bb19e2112e7030dad57dab8c9b8d142b1046f21
7
+ data.tar.gz: ffe79dd1557c4d40bc9f04eb737964cadd807c8b4f72a3615e1d13b7d6fb5e15c7c5cc8732f8e6b57aa8a36d8d370c83b6ae571ad68cfe217751cee3db3824a0
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,62 @@
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 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
+ matrix = {
37
+ platform: [],
38
+ collection: %w[
39
+ puppet5
40
+ puppet6
41
+ puppet7-nightly
42
+ ],
43
+ }
44
+
45
+ metadata = JSON.parse(File.read('metadata.json'))
46
+ metadata['operatingsystem_support'].sort_by { |a| a['operatingsystem'] }.each do |sup|
47
+ os = sup['operatingsystem']
48
+ sup['operatingsystemrelease'].sort_by { |a| a.to_i }.each do |ver|
49
+ image_key = "#{os}-#{ver}"
50
+ if IMAGE_TABLE.key? image_key
51
+ matrix[:platform] << IMAGE_TABLE[image_key]
52
+ elsif DOCKER_PLATFORMS.include? image_key
53
+ puts "Expecting #{image_key} test using docker on travis"
54
+ else
55
+ puts "::warning::Cannot find image for #{image_key}"
56
+ end
57
+ end
58
+ end
59
+
60
+ puts "::set-output name=matrix::#{JSON.generate(matrix)}"
61
+
62
+ 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['HTTP_X_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['HTTP_X_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['HTTP_X_HONEYCOMB_TRACE'] = span.to_trace_header
121
121
  span.add_field('litmus.manifest', manifest)
122
122
 
123
123
  require 'tmpdir'
@@ -135,7 +135,7 @@ module PuppetLitmus::PuppetHelpers
135
135
  span.add_field('litmus.node_name', target_node_name)
136
136
  add_platform_field(inventory_hash, target_node_name)
137
137
 
138
- manifest_file_location = "/tmp/#{File.basename(manifest_file)}"
138
+ manifest_file_location = File.basename(manifest_file)
139
139
  bolt_result = upload_file(manifest_file.path, manifest_file_location, target_node_name, options: {}, config: nil, inventory: inventory_hash)
140
140
  span.add_field('litmus.bolt_result', bolt_result)
141
141
  raise bolt_result.first['value'].to_s unless bolt_result.first['status'] == 'success'
@@ -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['HTTP_X_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['HTTP_X_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['HTTP_X_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['HTTP_X_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['HTTP_X_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['HTTP_X_HONEYCOMB_TRACE'])
13
+ ENV['HTTP_X_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')
@@ -42,7 +48,7 @@ end
42
48
  module PuppetLitmus::RakeHelper
43
49
  # DEFAULT_CONFIG_DATA should be frozen for our safety, but it needs to work around https://github.com/puppetlabs/bolt/pull/1696
44
50
  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
51
+ SUPPORTED_PROVISIONERS ||= %w[abs docker docker_exp provision_service vagrant vmpooler].freeze
46
52
 
47
53
  # Gets a string representing the operating system and version.
48
54
  #
@@ -90,7 +96,7 @@ module PuppetLitmus::RakeHelper
90
96
  # @return [Object] the standard out stream.
91
97
  def run_local_command(command)
92
98
  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']
99
+ ENV['HTTP_X_HONEYCOMB_TRACE'] = span.to_trace_header
94
100
  span.add_field('litmus.command', command)
95
101
 
96
102
  require 'open3'
@@ -113,14 +119,14 @@ module PuppetLitmus::RakeHelper
113
119
 
114
120
  Honeycomb.add_field_to_trace('litmus.provisioner', provisioner)
115
121
  Honeycomb.start_span(name: 'litmus.provision') do |span|
116
- ENV['HTTP_X_HONEYCOMB_TRACE'] = span.to_trace_header unless ENV['HTTP_X_HONEYCOMB_TRACE']
122
+ ENV['HTTP_X_HONEYCOMB_TRACE'] = span.to_trace_header
117
123
  span.add_field('litmus.platform', platform)
118
124
  span.add_field('litmus.inventory', params['inventory'])
119
125
  span.add_field('litmus.config', DEFAULT_CONFIG_DATA)
120
126
 
121
127
  bolt_result = run_task(provisioner_task(provisioner), 'localhost', params, config: DEFAULT_CONFIG_DATA, inventory: nil)
122
-
123
128
  span.add_field('litmus.node_name', bolt_result&.first&.dig('value', 'node_name'))
129
+ raise_bolt_errors(bolt_result, "provisioning of #{platform} failed.")
124
130
 
125
131
  bolt_result
126
132
  end
@@ -142,7 +148,7 @@ module PuppetLitmus::RakeHelper
142
148
 
143
149
  def tear_down_nodes(targets, inventory_hash)
144
150
  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']
151
+ ENV['HTTP_X_HONEYCOMB_TRACE'] = span.to_trace_header
146
152
  span.add_field('litmus.targets', targets)
147
153
 
148
154
  include ::BoltSpec::Run
@@ -162,7 +168,7 @@ module PuppetLitmus::RakeHelper
162
168
 
163
169
  def tear_down(node_name, inventory_hash)
164
170
  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']
171
+ ENV['HTTP_X_HONEYCOMB_TRACE'] = span.to_trace_header
166
172
  # how do we know what provisioner to use
167
173
 
168
174
  span.add_field('litmus.node_name', node_name)
@@ -170,13 +176,15 @@ module PuppetLitmus::RakeHelper
170
176
 
171
177
  params = { 'action' => 'tear_down', 'node_name' => node_name, 'inventory' => Dir.pwd }
172
178
  node_facts = facts_from_node(inventory_hash, node_name)
173
- run_task(provisioner_task(node_facts['provisioner']), 'localhost', params, config: DEFAULT_CONFIG_DATA, inventory: nil)
179
+ bolt_result = run_task(provisioner_task(node_facts['provisioner']), 'localhost', params, config: DEFAULT_CONFIG_DATA, inventory: nil)
180
+ raise_bolt_errors(bolt_result, "tear_down of #{node_name} failed.")
181
+ bolt_result
174
182
  end
175
183
  end
176
184
 
177
185
  def install_agent(collection, targets, inventory_hash)
178
186
  Honeycomb.start_span(name: 'litmus.install_agent') do |span|
179
- ENV['HTTP_X_HONEYCOMB_TRACE'] = span.to_trace_header unless ENV['HTTP_X_HONEYCOMB_TRACE']
187
+ ENV['HTTP_X_HONEYCOMB_TRACE'] = span.to_trace_header
180
188
  span.add_field('litmus.collection', collection)
181
189
  span.add_field('litmus.targets', targets)
182
190
 
@@ -184,7 +192,6 @@ module PuppetLitmus::RakeHelper
184
192
  params = if collection.nil?
185
193
  {}
186
194
  else
187
- Honeycomb.current_span.add_field('litmus.collection', collection)
188
195
  { 'collection' => collection }
189
196
  end
190
197
  raise "puppet_agent was not found in #{DEFAULT_CONFIG_DATA['modulepath']}, please amend the .fixtures.yml file" \
@@ -200,81 +207,102 @@ module PuppetLitmus::RakeHelper
200
207
  def configure_path(inventory_hash)
201
208
  results = []
202
209
  # fix the path on ssh_nodes
203
- unless inventory_hash['groups'].select { |group| group['name'] == 'ssh_nodes' }.size.zero?
204
- results = run_command('echo PATH="$PATH:/opt/puppetlabs/puppet/bin" > /etc/environment',
205
- 'ssh_nodes', config: nil, inventory: inventory_hash)
210
+ unless inventory_hash['groups'].select { |group| group['name'] == 'ssh_nodes' && !group['targets'].empty? }.size.zero?
211
+ results << run_command('echo PATH="$PATH:/opt/puppetlabs/puppet/bin" > /etc/environment',
212
+ 'ssh_nodes', config: nil, inventory: inventory_hash)
213
+ end
214
+ unless inventory_hash['groups'].select { |group| group['name'] == 'winrm_nodes' && !group['targets'].empty? }.size.zero?
215
+ results << run_command('[Environment]::SetEnvironmentVariable("Path", $env:Path + ";C:\Program Files\Puppet Labs\Puppet\bin;C:\Program Files (x86)\Puppet Labs\Puppet\bin", "Machine")',
216
+ 'winrm_nodes', config: nil, inventory: inventory_hash)
206
217
  end
207
218
  results
208
219
  end
209
220
 
221
+ # Build the module in `module_dir` and put the resulting compressed tarball into `target_dir`.
222
+ #
210
223
  # @param opts Hash of options to build the module
211
224
  # @param module_dir [String] The path of the module to build. If missing defaults to Dir.pwd
212
- # @param target_dir [String] The path the module will be built into. The default is <source_dir>/pkg
225
+ # @param target_dir [String] The path the module will be built into. The default is <module_dir>/pkg
213
226
  # @return [String] The path to the built module
214
227
  def build_module(module_dir = nil, target_dir = nil)
215
228
  require 'puppet/modulebuilder'
216
229
 
217
- source_dir = module_dir || Dir.pwd
218
- dest_dir = target_dir || File.join(source_dir, 'pkg')
230
+ module_dir ||= Dir.pwd
231
+ target_dir ||= File.join(source_dir, 'pkg')
232
+
233
+ puts "Building '#{module_dir}' into '#{target_dir}'"
234
+ builder = Puppet::Modulebuilder::Builder.new(module_dir, target_dir, nil)
219
235
 
220
- builder = Puppet::Modulebuilder::Builder.new(source_dir, dest_dir, nil)
221
236
  # Force the metadata to be read. Raises if metadata could not be found
222
237
  _metadata = builder.metadata
223
238
 
224
239
  builder.build
225
240
  end
226
241
 
227
- def install_module(inventory_hash, target_node_name, module_tar, module_repository = 'https://forgeapi.puppetlabs.com')
242
+ # Builds all the modules in a specified directory
243
+ #
244
+ # @param source_dir [String] the directory to get the modules from
245
+ # @param target_dir [String] temporary location to store tarballs before uploading. This directory will be cleaned before use. The default is <source_dir>/pkg
246
+ # @return [Array] an array of module tars' filenames
247
+ def build_modules_in_dir(source_dir, target_dir = nil)
248
+ target_dir ||= File.join(Dir.pwd, 'pkg')
249
+ # remove old build dir if exists, before we build afresh
250
+ FileUtils.rm_rf(target_dir) if File.directory?(target_dir)
251
+
252
+ module_tars = Dir.entries(source_dir).map do |entry|
253
+ next if ['.', '..'].include? entry
254
+
255
+ module_dir = File.join(source_dir, entry)
256
+ next unless File.directory? module_dir
257
+
258
+ build_module(module_dir, target_dir)
259
+ end
260
+ module_tars.compact
261
+ end
262
+
263
+ # @deprecated Use `build_modules_in_dir` instead
264
+ def build_modules_in_folder(source_folder)
265
+ build_modules_in_dir(source_folder)
266
+ end
267
+
268
+ # Install a specific module tarball to the specified target.
269
+ # This method installs dependencies using a forge repository.
270
+ #
271
+ # @param inventory_hash [Hash] the pre-loaded inventory
272
+ # @param target_node_name [String] the name of the target where the module should be installed
273
+ # @param module_tar [String] the filename of the module tarball to upload
274
+ # @param module_repository [String] the URL for the forge to use for downloading modules. Defaults to the public Forge API.
275
+ # @param ignore_dependencies [Boolean] flag used to ignore module dependencies defaults to false.
276
+ # @return a bolt result
277
+ def install_module(inventory_hash, target_node_name, module_tar, module_repository = nil, ignore_dependencies = false) # rubocop:disable Style/OptionalBooleanParameter
228
278
  Honeycomb.start_span(name: 'install_module') do |span|
229
- ENV['HTTP_X_HONEYCOMB_TRACE'] = span.to_trace_header unless ENV['HTTP_X_HONEYCOMB_TRACE']
279
+ ENV['HTTP_X_HONEYCOMB_TRACE'] = span.to_trace_header
230
280
  span.add_field('litmus.target_node_name', target_node_name)
231
281
  span.add_field('litmus.module_tar', module_tar)
232
282
 
233
- # make sure the target module is not installed
283
+ # make sure the module to install is not installed
234
284
  # otherwise `puppet module install` might silently skip it
235
- uninstall_module(inventory_hash.clone, target_node_name, force: true)
285
+ module_name = File.basename(module_tar, '.tar.gz').split('-', 3)[0..1].join('-')
286
+ uninstall_module(inventory_hash.clone, target_node_name, module_name, force: true)
236
287
 
237
288
  include ::BoltSpec::Run
238
289
 
239
290
  target_nodes = find_targets(inventory_hash, target_node_name)
240
291
  span.add_field('litmus.target_nodes', target_nodes)
241
- bolt_result = upload_file(module_tar, "/tmp/#{File.basename(module_tar)}", target_nodes, options: {}, config: nil, inventory: inventory_hash.clone)
292
+ bolt_result = upload_file(module_tar, File.basename(module_tar), target_nodes, options: {}, config: nil, inventory: inventory_hash.clone)
242
293
  raise_bolt_errors(bolt_result, 'Failed to upload module.')
243
294
 
244
- install_module_command = "puppet module install --module_repository '#{module_repository}' /tmp/#{File.basename(module_tar)}"
295
+ module_repository_opts = "--module_repository '#{module_repository}'" unless module_repository.nil?
296
+ install_module_command = "puppet module install #{module_repository_opts} #{File.basename(module_tar)}"
297
+ install_module_command += ' --ignore-dependencies --force' if ignore_dependencies.to_s.downcase == 'true'
245
298
  span.add_field('litmus.install_module_command', install_module_command)
246
299
 
247
300
  bolt_result = run_command(install_module_command, target_nodes, config: nil, inventory: inventory_hash.clone)
248
- raise_bolt_errors(bolt_result, "Installation of package #{module_tar} failed.")
301
+ raise_bolt_errors(bolt_result, "Installation of package #{File.basename(module_tar)} failed.")
249
302
  bolt_result
250
303
  end
251
304
  end
252
305
 
253
- # Builds all the modules in a specified module
254
- #
255
- # @param source_folder [String] the folder to get the modules from
256
- # @return [Array] an array of module tar's
257
- def build_modules_in_folder(source_folder)
258
- folder_list = Dir.entries(source_folder).reject { |f| File.directory? f }
259
- module_tars = []
260
-
261
- target_dir = File.join(Dir.pwd, 'pkg')
262
- # remove old build folder if exists, before we build afresh
263
- FileUtils.rm_rf(target_dir) if File.directory?(target_dir)
264
-
265
- folder_list.each do |folder|
266
- folder_handle = Dir.open(File.join(source_folder, folder))
267
- next if File.symlink?(folder_handle)
268
-
269
- module_dir = folder_handle.path
270
-
271
- # build_module
272
- module_tar = build_module(module_dir, target_dir)
273
- module_tars.push(File.new(module_tar))
274
- end
275
- module_tars
276
- end
277
-
278
306
  def metadata_module_name
279
307
  require 'json'
280
308
  raise 'Could not find metadata.json' unless File.exist?(File.join(Dir.pwd, 'metadata.json'))
@@ -285,33 +313,46 @@ module PuppetLitmus::RakeHelper
285
313
  metadata['name']
286
314
  end
287
315
 
316
+ # Uninstall a module from a specified target
317
+ # @param inventory_hash [Hash] the pre-loaded inventory
318
+ # @param target_node_name [String] the name of the target where the module should be uninstalled
319
+ # @param module_to_remove [String] the name of the module to remove. Defaults to the module under test.
320
+ # @param opts [Hash] additional options to pass on to `puppet module uninstall`
288
321
  def uninstall_module(inventory_hash, target_node_name, module_to_remove = nil, **opts)
289
322
  include ::BoltSpec::Run
290
323
  module_name = module_to_remove || metadata_module_name
291
324
  target_nodes = find_targets(inventory_hash, target_node_name)
292
325
  install_module_command = "puppet module uninstall #{module_name}"
293
326
  install_module_command += ' --force' if opts[:force]
294
- run_command(install_module_command, target_nodes, config: nil, inventory: inventory_hash)
327
+ bolt_result = run_command(install_module_command, target_nodes, config: nil, inventory: inventory_hash)
328
+ # `puppet module uninstall --force` fails if the module is not installed. Ignore errors when force is set
329
+ raise_bolt_errors(bolt_result, "uninstalling #{module_name} failed.") unless opts[:force]
330
+ bolt_result
295
331
  end
296
332
 
297
333
  def check_connectivity?(inventory_hash, target_node_name)
298
334
  Honeycomb.start_span(name: 'litmus.check_connectivity') do |span|
299
- ENV['HTTP_X_HONEYCOMB_TRACE'] = span.to_trace_header unless ENV['HTTP_X_HONEYCOMB_TRACE']
335
+ ENV['HTTP_X_HONEYCOMB_TRACE'] = span.to_trace_header
300
336
  # if we're only checking connectivity for a single node
301
337
  if target_node_name
302
- span.add_field('litmus.node_name', target_node_name)
338
+ span.add_field('litmus.target_node_name', target_node_name)
303
339
  add_platform_field(inventory_hash, target_node_name)
304
340
  end
305
341
 
306
342
  include ::BoltSpec::Run
307
343
  target_nodes = find_targets(inventory_hash, target_node_name)
344
+ puts "Checking connectivity for #{target_nodes.inspect}"
345
+ span.add_field('litmus.target_nodes', target_nodes)
346
+
308
347
  results = run_command('cd .', target_nodes, config: nil, inventory: inventory_hash)
309
348
  span.add_field('litmus.bolt_result', results)
310
349
  failed = []
311
- results.each do |result|
312
- failed.push(result['target']) if result['status'] == 'failure'
350
+ results.reject { |r| r['status'] == 'success' }.each do |result|
351
+ puts "Failure connecting to #{result['target']}:\n#{result.inspect}"
352
+ failed.push(result['target'])
313
353
  end
314
- span.add_field('litmus.connectivity_failed', failed)
354
+ span.add_field('litmus.connectivity_success', results.select { |r| r['status'] == 'success' })
355
+ span.add_field('litmus.connectivity_failure', results.reject { |r| r['status'] == 'success' })
315
356
  raise "Connectivity has failed on: #{failed}" unless failed.length.zero?
316
357
 
317
358
  true
@@ -330,7 +371,7 @@ module PuppetLitmus::RakeHelper
330
371
  # Parse out errors messages in result set returned by Bolt command.
331
372
  #
332
373
  # @param result_set [Array] result set returned by Bolt command.
333
- # @return [Hash] Error messages grouped by target.
374
+ # @return [Hash] Errors grouped by target.
334
375
  def check_bolt_errors(result_set)
335
376
  errors = {}
336
377
  # iterate through each error
@@ -341,8 +382,7 @@ module PuppetLitmus::RakeHelper
341
382
 
342
383
  target = target_result['target']
343
384
  # get some info from error
344
- error_msg = target_result['value']['_error']['msg']
345
- errors[target] = error_msg
385
+ errors[target] = target_result['value']
346
386
  end
347
387
  errors
348
388
  end
@@ -355,9 +395,56 @@ module PuppetLitmus::RakeHelper
355
395
  errors = check_bolt_errors(result_set)
356
396
 
357
397
  unless errors.empty?
358
- raise "#{error_msg}\nErrors: #{errors}"
398
+ formatted_results = errors.map { |k, v| " #{k}: #{v.inspect}" }.join("\n")
399
+ raise "#{error_msg}\nResults:\n#{formatted_results}}"
359
400
  end
360
401
 
361
402
  nil
362
403
  end
404
+
405
+ def start_spinner(message)
406
+ if (ENV['CI'] || '').downcase == 'true'
407
+ puts message
408
+ spinner = Thread.new do
409
+ # CI systems are strange beasts, we only output a '.' every wee while to keep the terminal alive.
410
+ loop do
411
+ printf '.'
412
+ sleep(10)
413
+ end
414
+ end
415
+ else
416
+ require 'tty-spinner'
417
+ spinner = TTY::Spinner.new("[:spinner] #{message}")
418
+ spinner.auto_spin
419
+ end
420
+ spinner
421
+ end
422
+
423
+ def stop_spinner(spinner)
424
+ if (ENV['CI'] || '').downcase == 'true'
425
+ Thread.kill(spinner)
426
+ else
427
+ spinner.success
428
+ end
429
+ end
430
+
431
+ require 'retryable'
432
+
433
+ Retryable.configure do |config|
434
+ config.sleep = ->(n) { (1.5**n) + Random.rand(0.5) }
435
+ # config.log_method = ->(retries, exception) do
436
+ # Logger.new($stdout).debug("[Attempt ##{retries}] Retrying because [#{exception.class} - #{exception.message}]: #{exception.backtrace.first(5).join(' | ')}")
437
+ # end
438
+ end
439
+
440
+ class LitmusTimeoutError < StandardError; end
441
+
442
+ def with_retries(options: { tries: Float::INFINITY }, max_wait_minutes: 5)
443
+ stop = Time.now + (max_wait_minutes * 60)
444
+ Retryable.retryable(options.merge(not: [LitmusTimeoutError])) do
445
+ raise LitmusTimeoutError if Time.now > stop
446
+
447
+ yield
448
+ end
449
+ end
363
450
  end