puppet_litmus 0.18.2 → 0.21.0

Sign up to get free protection for your applications and to get access to all the features.
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