simp-beaker-helpers 1.18.8

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.
Files changed (56) hide show
  1. checksums.yaml +7 -0
  2. data/.fixtures.yml +8 -0
  3. data/.gitignore +8 -0
  4. data/.gitlab-ci.yml +163 -0
  5. data/.rspec +4 -0
  6. data/.rubocop.yml +546 -0
  7. data/.travis.yml +36 -0
  8. data/CHANGELOG.md +231 -0
  9. data/Gemfile +51 -0
  10. data/LICENSE +27 -0
  11. data/README.md +543 -0
  12. data/Rakefile +151 -0
  13. data/files/pki/clean.sh +1 -0
  14. data/files/pki/make.sh +101 -0
  15. data/files/pki/template_ca.cnf +259 -0
  16. data/files/pki/template_host.cnf +263 -0
  17. data/files/puppet-agent-versions.yaml +46 -0
  18. data/lib/simp/beaker_helpers.rb +1231 -0
  19. data/lib/simp/beaker_helpers/constants.rb +25 -0
  20. data/lib/simp/beaker_helpers/inspec.rb +328 -0
  21. data/lib/simp/beaker_helpers/snapshot.rb +156 -0
  22. data/lib/simp/beaker_helpers/ssg.rb +383 -0
  23. data/lib/simp/beaker_helpers/version.rb +5 -0
  24. data/lib/simp/beaker_helpers/windows.rb +16 -0
  25. data/lib/simp/rake/beaker.rb +269 -0
  26. data/simp-beaker-helpers.gemspec +38 -0
  27. data/spec/acceptance/nodesets/default.yml +32 -0
  28. data/spec/acceptance/suites/default/check_puppet_version_spec.rb +23 -0
  29. data/spec/acceptance/suites/default/enable_fips_spec.rb +23 -0
  30. data/spec/acceptance/suites/default/fixture_modules_spec.rb +22 -0
  31. data/spec/acceptance/suites/default/install_simp_deps_repo_spec.rb +43 -0
  32. data/spec/acceptance/suites/default/nodesets +1 -0
  33. data/spec/acceptance/suites/default/pki_tests_spec.rb +55 -0
  34. data/spec/acceptance/suites/default/set_hieradata_on_spec.rb +33 -0
  35. data/spec/acceptance/suites/default/write_hieradata_to_spec.rb +33 -0
  36. data/spec/acceptance/suites/fips_from_fixtures/00_default_spec.rb +63 -0
  37. data/spec/acceptance/suites/fips_from_fixtures/metadata.yml +2 -0
  38. data/spec/acceptance/suites/fips_from_fixtures/nodesets +1 -0
  39. data/spec/acceptance/suites/offline/00_default_spec.rb +165 -0
  40. data/spec/acceptance/suites/offline/README +2 -0
  41. data/spec/acceptance/suites/offline/nodesets/default.yml +26 -0
  42. data/spec/acceptance/suites/puppet_collections/00_default_spec.rb +25 -0
  43. data/spec/acceptance/suites/puppet_collections/metadata.yml +2 -0
  44. data/spec/acceptance/suites/puppet_collections/nodesets/default.yml +30 -0
  45. data/spec/acceptance/suites/snapshot/00_snapshot_test_spec.rb +82 -0
  46. data/spec/acceptance/suites/snapshot/10_general_usage_spec.rb +56 -0
  47. data/spec/acceptance/suites/snapshot/nodesets +1 -0
  48. data/spec/acceptance/suites/windows/00_default_spec.rb +119 -0
  49. data/spec/acceptance/suites/windows/metadata.yml +2 -0
  50. data/spec/acceptance/suites/windows/nodesets/default.yml +33 -0
  51. data/spec/acceptance/suites/windows/nodesets/win2016.yml +35 -0
  52. data/spec/acceptance/suites/windows/nodesets/win2019.yml +34 -0
  53. data/spec/lib/simp/beaker_helpers_spec.rb +216 -0
  54. data/spec/spec_helper.rb +100 -0
  55. data/spec/spec_helper_acceptance.rb +25 -0
  56. metadata +243 -0
@@ -0,0 +1,25 @@
1
+ module Simp; end
2
+
3
+ module Simp::BeakerHelpers
4
+ # This is the *oldest* puppet-agent version that the latest release of SIMP supports
5
+ #
6
+ # This is done so that we know if some new thing that we're using breaks the
7
+ # oldest system that we support
8
+ DEFAULT_PUPPET_AGENT_VERSION = '~> 5.0'
9
+
10
+ SSG_REPO_URL = ENV['BEAKER_ssg_repo'] || 'https://github.com/ComplianceAsCode/content.git'
11
+
12
+ if ['true','yes'].include?(ENV['BEAKER_online'])
13
+ ONLINE = true
14
+ elsif ['false','no'].include?(ENV['BEAKER_online'])
15
+ ONLINE = false
16
+ else
17
+ require 'open-uri'
18
+
19
+ begin
20
+ ONLINE = true if open('http://google.com')
21
+ rescue
22
+ ONLINE = false
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,328 @@
1
+ module Simp::BeakerHelpers
2
+ require 'simp/beaker_helpers/constants'
3
+
4
+ # Helpers for working with Inspec
5
+ class Inspec
6
+
7
+ require 'json'
8
+
9
+ attr_reader :profile
10
+ attr_reader :profile_dir
11
+ attr_reader :deps_root
12
+
13
+ # Create a new Inspec helper for the specified host against the specified profile
14
+ #
15
+ # @param sut
16
+ # The SUT against which to run
17
+ #
18
+ # @param profile
19
+ # The name of the profile against which to run
20
+ #
21
+ def initialize(sut, profile)
22
+ @inspec_version = ENV['BEAKER_inspec_version'] || 'latest'
23
+
24
+ @sut = sut
25
+
26
+ @sut.install_package('git')
27
+
28
+ if @inspec_version == 'latest'
29
+ @sut.install_package('inspec')
30
+ else
31
+ @sut.install_package("inspec-#{@inspec_version}")
32
+ end
33
+
34
+ os = fact_on(@sut, 'operatingsystem')
35
+ os_rel = fact_on(@sut, 'operatingsystemmajrelease')
36
+
37
+ @profile = "#{os}-#{os_rel}-#{profile}"
38
+ @profile_dir = '/tmp/inspec/inspec_profiles'
39
+ @deps_root = '/tmp/inspec'
40
+
41
+ @test_dir = @profile_dir + "/#{@profile}"
42
+
43
+ sut.mkdir_p(@profile_dir)
44
+
45
+ output_dir = File.absolute_path('sec_results/inspec')
46
+
47
+ unless File.directory?(output_dir)
48
+ FileUtils.mkdir_p(output_dir)
49
+ end
50
+
51
+ local_profile = File.join(fixtures_path, 'inspec_profiles', %(#{os}-#{os_rel}-#{profile}))
52
+ local_deps = File.join(fixtures_path, 'inspec_deps')
53
+
54
+ @result_file = File.join(output_dir, "#{@sut.hostname}-inspec-#{Time.now.to_i}")
55
+
56
+ copy_to(@sut, local_profile, @profile_dir)
57
+
58
+ if File.exist?(local_deps)
59
+ copy_to(@sut, local_deps, @deps_root)
60
+ end
61
+
62
+ # The results of the inspec scan in Hash form
63
+ @results = {}
64
+ end
65
+
66
+ # Run the inspec tests and record the results
67
+ def run
68
+ sut_inspec_results = '/tmp/inspec_results.json'
69
+
70
+ inspec_version = Gem::Version.new(on(@sut, 'inspec --version').output.lines.first.strip)
71
+
72
+ # See: https://github.com/inspec/inspec/pull/3935
73
+ if inspec_version <= Gem::Version.new('3.9.0')
74
+ inspec_cmd = "inspec exec '#{@test_dir}' --reporter json > #{sut_inspec_results}"
75
+ else
76
+ inspec_cmd = "inspec exec '#{@test_dir}' --chef-license accept --reporter json > #{sut_inspec_results}"
77
+ end
78
+
79
+ result = on(@sut, inspec_cmd, :accept_all_exit_codes => true)
80
+
81
+ tmpdir = Dir.mktmpdir
82
+ begin
83
+ Dir.chdir(tmpdir) do
84
+ if @sut[:hypervisor] == 'docker'
85
+ # Work around for breaking changes in beaker-docker
86
+ if @sut.host_hash[:docker_container]
87
+ container_id = @sut.host_hash[:docker_container].id
88
+ else
89
+ container_id = @sut.host_hash[:docker_container_id]
90
+ end
91
+
92
+ %x(docker cp "#{container_id}:#{sut_inspec_results}" .)
93
+ else
94
+ scp_from(@sut, sut_inspec_results, '.')
95
+ end
96
+
97
+ local_inspec_results = File.basename(sut_inspec_results)
98
+
99
+ if File.exist?(local_inspec_results)
100
+ begin
101
+ # The output is occasionally broken from past experience. Need to
102
+ # fetch the line that actually looks like JSON
103
+ inspec_json = File.read(local_inspec_results).lines.find do |line|
104
+ line.strip!
105
+
106
+ line.start_with?('{') && line.end_with?('}')
107
+ end
108
+
109
+ @results = JSON.load(inspec_json) if inspec_json
110
+ rescue JSON::ParserError, JSON::GeneratorError
111
+ @results = nil
112
+ end
113
+ end
114
+ end
115
+ ensure
116
+ FileUtils.remove_entry_secure tmpdir
117
+ end
118
+
119
+ if @results.nil? || @results.empty?
120
+ File.open(@result_file + '.err', 'w') do |fh|
121
+ fh.puts(result.stderr.strip)
122
+ end
123
+
124
+ err_msg = ["Error running inspec command #{inspec_cmd}"]
125
+ err_msg << "Error captured in #{@result_file}" + '.err'
126
+
127
+ fail(err_msg.join("\n"))
128
+ end
129
+ end
130
+
131
+ # Output the report
132
+ #
133
+ # @param report
134
+ # The inspec results Hash
135
+ #
136
+ def write_report(report)
137
+ File.open(@result_file + '.json', 'w') do |fh|
138
+ fh.puts(JSON.pretty_generate(@results))
139
+ end
140
+
141
+ File.open(@result_file + '.report', 'w') do |fh|
142
+ fh.puts(report[:report].uncolor)
143
+ end
144
+ end
145
+
146
+ def process_inspec_results
147
+ self.class.process_inspec_results(@results)
148
+ end
149
+
150
+ # Process the results of an InSpec run
151
+ #
152
+ # @return [Hash] A Hash of statistics and a formatted report
153
+ #
154
+ def self.process_inspec_results(results)
155
+ require 'highline'
156
+
157
+ HighLine.colorize_strings
158
+
159
+ stats = {
160
+ # Legacy metrics counters for backwards compatibility
161
+ :failed => 0,
162
+ :passed => 0,
163
+ :skipped => 0,
164
+ :overridden => 0,
165
+ # End legacy stuff
166
+ :global => {
167
+ :failed => [],
168
+ :passed => [],
169
+ :skipped => [],
170
+ :overridden => []
171
+ },
172
+ :score => 0,
173
+ :report => nil,
174
+ :profiles => {}
175
+ }
176
+
177
+ if results.is_a?(String)
178
+ if File.readable?(results)
179
+ profiles = JSON.load(File.read(results))['profiles']
180
+ else
181
+ fail("Error: Could not read results file at #{results}")
182
+ end
183
+ elsif results.is_a?(Hash)
184
+ profiles = results['profiles']
185
+ else
186
+ fail("Error: first argument must be a String path to a file or a Hash")
187
+ end
188
+
189
+ if !profiles || profiles.empty?
190
+ fail("Error: Could not find 'profiles' in the passed results")
191
+ end
192
+
193
+ profiles.each do |profile|
194
+ profile_name = profile['name']
195
+
196
+ next unless profile_name
197
+
198
+ stats[:profiles][profile_name] = {
199
+ :controls => {}
200
+ }
201
+
202
+ profile['controls'].each do |control|
203
+ title = control['title']
204
+
205
+ next unless title
206
+
207
+ base_title = title.scan(/.{1,60}\W|.{1,60}/).map(&:strip).join("\n ")
208
+
209
+ if control['results'] && (control['results'].size > 1)
210
+ control['results'].each do |result|
211
+ control_title = " => { #{result['code_desc']} }"
212
+
213
+ full_title = title + control_title
214
+ formatted_title = base_title + control_title
215
+
216
+ stats[:profiles][profile_name][:controls][full_title] = {}
217
+
218
+ stats[:profiles][profile_name][:controls][full_title][:formatted_title] = formatted_title
219
+
220
+ if result['status'] =~ /^fail/
221
+ status = :failed
222
+ color = 'red'
223
+ else
224
+ status = :passed
225
+ color = 'green'
226
+ end
227
+
228
+ stats[:global][status] << formatted_title.color
229
+
230
+ stats[:profiles][profile_name][:controls][full_title][:status] = status
231
+ stats[:profiles][profile_name][:controls][full_title][:source] = control['source_location']['ref']
232
+ end
233
+ else
234
+ formatted_title = base_title
235
+
236
+ stats[:profiles][profile_name][:controls][title] = {}
237
+
238
+ stats[:profiles][profile_name][:controls][title][:formatted_title] = formatted_title
239
+
240
+ if control['results'] && !control['results'].empty?
241
+ status = :passed
242
+ color = 'green'
243
+
244
+ control['results'].each do |result|
245
+ if results['status'] =~ /^fail/
246
+ status = :failed
247
+ color = 'red'
248
+ end
249
+ end
250
+
251
+ else
252
+ status = :skipped
253
+ end
254
+
255
+ stats[:global][status] << formatted_title.color
256
+
257
+ stats[:profiles][profile_name][:controls][title][:status] = status
258
+ stats[:profiles][profile_name][:controls][title][:source] = control['source_location']['ref']
259
+ end
260
+ end
261
+ end
262
+
263
+ valid_checks = stats[:global][:failed] + stats[:global][:passed]
264
+ stats[:global][:skipped].dup.each do |skipped|
265
+ if valid_checks.include?(skipped)
266
+ stats[:global][:overridden] << skipped
267
+ stats[:global][:skipped].delete(skipped)
268
+ end
269
+ end
270
+
271
+ status_colors = {
272
+ :failed => 'red',
273
+ :passed => 'green',
274
+ :skipped => 'yellow',
275
+ :overridden => 'white'
276
+ }
277
+
278
+ report = []
279
+
280
+ stats[:profiles].keys.each do |profile|
281
+ report << "Profile: #{profile}"
282
+
283
+ stats[:profiles][profile][:controls].each do |control|
284
+ control_info = control.last
285
+
286
+ report << "\n Control: #{control_info[:formatted_title]}"
287
+
288
+ if control_info[:status] == :skipped && stats[:global][:overridden].include?(control.first)
289
+ control_info[:status] = :overridden
290
+ end
291
+
292
+ report << " Status: #{control_info[:status].to_s.send(status_colors[control_info[:status]])}"
293
+ report << " File: #{control_info[:source]}" if control_info[:source]
294
+ end
295
+
296
+ report << "\n"
297
+ end
298
+
299
+ num_passed = stats[:global][:passed].count
300
+ num_failed = stats[:global][:failed].count
301
+ num_skipped = stats[:global][:skipped].count
302
+ num_overridden = stats[:global][:overridden].count
303
+
304
+ # Backwards compat values
305
+ stats[:passed] = num_passed
306
+ stats[:failed] = num_failed
307
+ stats[:skipped] = num_skipped
308
+ stats[:overridden] = num_overridden
309
+
310
+ report << "Statistics:"
311
+ report << " * Passed: #{num_passed.to_s.green}"
312
+ report << " * Failed: #{num_failed.to_s.red}"
313
+ report << " * Skipped: #{num_skipped.to_s.yellow}"
314
+
315
+ score = 0
316
+ if (stats[:global][:passed].count + stats[:global][:failed].count) > 0
317
+ score = ((stats[:global][:passed].count.to_f/(stats[:global][:passed].count + stats[:global][:failed].count)) * 100.0).round(0)
318
+ end
319
+
320
+ report << "\n Score: #{score}%"
321
+
322
+ stats[:score] = score
323
+ stats[:report] = report.join("\n")
324
+
325
+ return stats
326
+ end
327
+ end
328
+ end
@@ -0,0 +1,156 @@
1
+ module Simp::BeakerHelpers
2
+ # Helpers for managing Vagrant snapshots
3
+ class Snapshot
4
+ # The name of the base snapshot that is created if no snapshots currently exist
5
+ BASE_NAME = '_simp_beaker_base'
6
+
7
+ # Save a snapshot
8
+ #
9
+ # @param host [Beaker::Host]
10
+ # The SUT to work on
11
+ #
12
+ # @param snapshot_name [String]
13
+ # The string to add to the snapshot
14
+ #
15
+ def self.save(host, snapshot_name)
16
+ if enabled?
17
+ vdir = vagrant_dir(host)
18
+
19
+ if vdir
20
+ Dir.chdir(vdir) do
21
+ save(host, BASE_NAME) unless exist?(host, BASE_NAME)
22
+
23
+ snap = "#{host.name}_#{snapshot_name}"
24
+
25
+ output = %x(vagrant snapshot save --force #{host.name} "#{snap}")
26
+
27
+ logger.notify(output)
28
+
29
+ retry_on(
30
+ host,
31
+ %(echo "saving snapshot '#{snap}'" > /dev/null),
32
+ :max_retries => 30,
33
+ :retry_interval => 1
34
+ )
35
+ end
36
+ end
37
+ end
38
+ end
39
+
40
+ # Whether or not a named snapshot exists
41
+ #
42
+ # @param host [Beaker::Host]
43
+ # The SUT to work on
44
+ #
45
+ # @param snapshot_name [String]
46
+ # The string to add to the snapshot
47
+ #
48
+ # @return [Boolean]
49
+ def self.exist?(host, name)
50
+ list(host).include?(name)
51
+ end
52
+
53
+ # List all snapshots for the given host
54
+ #
55
+ # @parma host [Beaker::Host]
56
+ # The SUT to work on
57
+ #
58
+ # @return [Array[String]]
59
+ # A list of snapshot names for the host
60
+ def self.list(host)
61
+ output = []
62
+ vdir = vagrant_dir(host)
63
+
64
+ if vdir
65
+ Dir.chdir(vdir) do
66
+ output = %x(vagrant snapshot list #{host.name}).lines
67
+ output.map! do |x|
68
+ x.split(/^#{host.name}_/).last.split(':').first.delete('==>').strip
69
+ end
70
+ end
71
+ end
72
+
73
+ output
74
+ end
75
+
76
+ # Restore a snapshot
77
+ #
78
+ # @param host [Beaker::Host]
79
+ # The SUT to work on
80
+ #
81
+ # @param snapshot_name [String]
82
+ # The name that was added to the snapshot
83
+ #
84
+ def self.restore(host, snapshot_name)
85
+ if enabled?
86
+ vdir = vagrant_dir(host)
87
+
88
+ if vdir
89
+ Dir.chdir(vdir) do
90
+ snap = "#{host.name}_#{snapshot_name}"
91
+
92
+ output = %x(vagrant snapshot restore #{host.name} "#{snap}" 2>&1)
93
+
94
+ if (output =~ /error/i) && (output =~ /child/)
95
+ raise output
96
+ end
97
+
98
+ if (output =~ /snapshot.*not found/)
99
+ raise output
100
+ end
101
+
102
+ logger.notify(output)
103
+
104
+ retry_on(
105
+ host,
106
+ %(echo "restoring snapshot '#{snap}'" > /dev/null),
107
+ :max_retries => 30,
108
+ :retry_interval => 1
109
+ )
110
+ end
111
+ end
112
+ end
113
+ end
114
+
115
+ # Restore all the way back to the base image
116
+ #
117
+ # @param host [Beaker::Host]
118
+ # The SUT to work on
119
+ #
120
+ def self.restore_to_base(host)
121
+ if exist?(host, BASE_NAME)
122
+ restore(host, BASE_NAME)
123
+ else
124
+ save(host, BASE_NAME)
125
+ end
126
+ end
127
+
128
+ private
129
+
130
+ def self.enabled?
131
+ enabled = ENV['BEAKER_simp_snapshot'] == 'yes'
132
+
133
+ unless enabled
134
+ logger.warn('Snapshotting not enabled, set BEAKER_simp_snapshot=yes to enable')
135
+ end
136
+
137
+ return enabled
138
+ end
139
+
140
+ def self.vagrant_dir(host)
141
+ tgt_dir = nil
142
+
143
+ if host && host.options && host.options[:hosts_file]
144
+ vdir = File.join('.vagrant', 'beaker_vagrant_files', File.basename(host.options[:hosts_file]))
145
+
146
+ if File.directory?(vdir)
147
+ tgt_dir = vdir
148
+ else
149
+ logger.notify("Could not find local vagrant dir at #{vdir}")
150
+ end
151
+ end
152
+
153
+ return tgt_dir
154
+ end
155
+ end
156
+ end