simp-beaker-helpers 1.18.8

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