cem_acpt 0.8.8 → 0.9.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/spec.yml +0 -3
  3. data/Gemfile.lock +9 -1
  4. data/README.md +95 -13
  5. data/cem_acpt.gemspec +2 -1
  6. data/lib/cem_acpt/action_result.rb +8 -2
  7. data/lib/cem_acpt/actions.rb +153 -0
  8. data/lib/cem_acpt/bolt/cmd/base.rb +174 -0
  9. data/lib/cem_acpt/bolt/cmd/output.rb +315 -0
  10. data/lib/cem_acpt/bolt/cmd/task.rb +59 -0
  11. data/lib/cem_acpt/bolt/cmd.rb +22 -0
  12. data/lib/cem_acpt/bolt/errors.rb +49 -0
  13. data/lib/cem_acpt/bolt/helpers.rb +52 -0
  14. data/lib/cem_acpt/bolt/inventory.rb +62 -0
  15. data/lib/cem_acpt/bolt/project.rb +38 -0
  16. data/lib/cem_acpt/bolt/summary_results.rb +96 -0
  17. data/lib/cem_acpt/bolt/tasks.rb +181 -0
  18. data/lib/cem_acpt/bolt/tests.rb +415 -0
  19. data/lib/cem_acpt/bolt/yaml_file.rb +74 -0
  20. data/lib/cem_acpt/bolt.rb +142 -0
  21. data/lib/cem_acpt/cli.rb +6 -0
  22. data/lib/cem_acpt/config/base.rb +4 -0
  23. data/lib/cem_acpt/config/cem_acpt.rb +7 -1
  24. data/lib/cem_acpt/core_ext.rb +25 -0
  25. data/lib/cem_acpt/goss/api/action_response.rb +4 -0
  26. data/lib/cem_acpt/goss/api.rb +23 -25
  27. data/lib/cem_acpt/image_builder/provision_commands.rb +43 -0
  28. data/lib/cem_acpt/logging/formatter.rb +3 -3
  29. data/lib/cem_acpt/logging.rb +17 -1
  30. data/lib/cem_acpt/provision/terraform/linux.rb +2 -2
  31. data/lib/cem_acpt/test_data.rb +2 -0
  32. data/lib/cem_acpt/test_runner/log_formatter/base.rb +73 -0
  33. data/lib/cem_acpt/test_runner/log_formatter/bolt_error_formatter.rb +65 -0
  34. data/lib/cem_acpt/test_runner/log_formatter/bolt_output_formatter.rb +54 -0
  35. data/lib/cem_acpt/test_runner/log_formatter/bolt_summary_results_formatter.rb +64 -0
  36. data/lib/cem_acpt/test_runner/log_formatter/goss_action_response.rb +17 -30
  37. data/lib/cem_acpt/test_runner/log_formatter/goss_error_formatter.rb +31 -0
  38. data/lib/cem_acpt/test_runner/log_formatter/standard_error_formatter.rb +35 -0
  39. data/lib/cem_acpt/test_runner/log_formatter.rb +17 -5
  40. data/lib/cem_acpt/test_runner/test_results.rb +150 -0
  41. data/lib/cem_acpt/test_runner.rb +153 -53
  42. data/lib/cem_acpt/utils/files.rb +189 -0
  43. data/lib/cem_acpt/utils/finalizer_queue.rb +73 -0
  44. data/lib/cem_acpt/utils/shell.rb +13 -4
  45. data/lib/cem_acpt/version.rb +1 -1
  46. data/sample_config.yaml +13 -0
  47. metadata +41 -5
  48. data/lib/cem_acpt/test_runner/log_formatter/error_formatter.rb +0 -33
@@ -2,7 +2,8 @@
2
2
 
3
3
  require 'fileutils'
4
4
  require 'securerandom'
5
- require_relative 'action_result'
5
+ require_relative 'actions'
6
+ require_relative 'bolt'
6
7
  require_relative 'goss'
7
8
  require_relative 'logging'
8
9
  require_relative 'platform'
@@ -11,6 +12,7 @@ require_relative 'test_data'
11
12
  require_relative 'utils'
12
13
  require_relative 'version'
13
14
  require_relative 'test_runner/log_formatter'
15
+ require_relative 'test_runner/test_results'
14
16
 
15
17
  module CemAcpt
16
18
  # Namespace for all Runner-related classes and modules
@@ -19,6 +21,8 @@ module CemAcpt
19
21
  class Runner
20
22
  include CemAcpt::Logging
21
23
 
24
+ SUCCESS_STATUS = [200, 0].freeze
25
+
22
26
  attr_reader :duration, :exit_code
23
27
  attr_accessor :run_data # This is opened up mainly for windows use.
24
28
 
@@ -27,8 +31,9 @@ module CemAcpt
27
31
  @run_data = {}
28
32
  @duration = 0
29
33
  @exit_code = 0
30
- @results = []
31
- @http_statuses = []
34
+ @bolt_test_runner = nil
35
+ @results = CemAcpt::TestRunner::TestResults.new
36
+ @statuses = []
32
37
  @provisioned = false
33
38
  @destroyed = false
34
39
  end
@@ -42,23 +47,14 @@ module CemAcpt
42
47
  end
43
48
 
44
49
  def run
45
- @run_data = {}
46
50
  @start_time = Time.now
47
51
  module_dir = config.get('module_dir')
48
52
  @old_dir = Dir.pwd
49
53
  Dir.chdir(module_dir)
54
+ configure_actions
50
55
  logger.start_ci_group("CemAcpt v#{CemAcpt::VERSION} run started at #{@start_time}")
51
56
  logger.info('CemAcpt::TestRunner') { "Using module directory: #{module_dir}..." }
52
- @run_data[:private_key], @run_data[:public_key], @run_data[:known_hosts] = new_ephemeral_ssh_keys
53
- logger.info('CemAcpt::TestRunner') { 'Created ephemeral SSH key pair...' }
54
- @run_data[:module_package_path] = build_module_package
55
- logger.info('CemAcpt::TestRunner') { "Created module package: #{@run_data[:module_package_path]}..." }
56
- @run_data[:test_data] = new_test_data
57
- logger.info('CemAcpt::TestRunner') { 'Created test data...' }
58
- logger.verbose('CemAcpt::TestRunner') { "Test data: #{@run_data[:test_data]}" }
59
- @run_data[:nodes] = new_node_data
60
- logger.info('CemAcpt::TestRunner') { 'Created node data...' }
61
- logger.verbose('CemAcpt::TestRunner') { "Node data: #{@run_data[:nodes]}" }
57
+ pre_provision_test_nodes
62
58
  provision_test_nodes
63
59
  @instance_names_ips = provisioner_output
64
60
  logger.info('CemAcpt::TestRunner') { "Instance names and IPs class: #{@instance_names_ips.class}" }
@@ -79,18 +75,17 @@ module CemAcpt
79
75
  win_node.run
80
76
  end
81
77
  end
82
- @results = run_tests(@instance_names_ips.map { |_, v| v['ip'] },
83
- config.get('actions.only'),
84
- config.get('actions.except'))
78
+ @hosts = @instance_names_ips.map { |_, v| v['ip'] }
79
+ run_tests
85
80
  rescue StandardError => e
86
81
  logger.error('CemAcpt::TestRunner') { 'Run failed due to error...' }
87
- @results << ActionResult.new(e)
82
+ @results << e
88
83
  ensure
89
84
  logger.end_ci_group
90
85
  clean_up
91
86
  process_test_results
92
87
  Dir.chdir(@old_dir) if @old_dir
93
- @results
88
+ @results.to_a
94
89
  end
95
90
 
96
91
  def clean_up(_trap_context = false)
@@ -117,13 +112,36 @@ module CemAcpt
117
112
 
118
113
  attr_reader :config
119
114
 
115
+ # Configures the actions to run based on the config
116
+ def configure_actions
117
+ logger.info('CemAcpt::TestRunner') { 'Configuring and registering actions...' }
118
+ goss_actions = CemAcpt::Goss::Api::ACTIONS.keys
119
+ CemAcpt::Actions.configure(config) do |c|
120
+ c.register_group(:goss, order: 0).register_action(goss_actions.first) do |context|
121
+ run_goss_tests(context)
122
+ end
123
+ goss_actions[1..-1].each { |a| c[:goss].register_action(a) }
124
+ c.register_group(:bolt, order: 1).register_action(:bolt) do |context|
125
+ run_bolt_tests(context)
126
+ end
127
+ end
128
+ logger.debug('CemAcpt::TestRunner') { "All actions #{CemAcpt::Actions.config.action_names}" }
129
+ logger.debug('CemAcpt::TestRunner') { "Only actions: #{CemAcpt::Actions.config.only}" }
130
+ logger.debug('CemAcpt::TestRunner') { "Except actions: #{CemAcpt::Actions.config.except}" }
131
+ logger.info('CemAcpt::TestRunner') do
132
+ "Configured and registered actions, will run actions: #{CemAcpt::Actions.config.action_names.join(', ')}"
133
+ end
134
+ end
135
+
120
136
  # @return [String] The path to the module package
121
137
  def build_module_package
122
- if config.get('tests').first.include? 'windows'
123
- CemAcpt::Utils.package_win_module(config.get('module_dir'))
124
- else
125
- CemAcpt::Utils::Puppet.build_module_package(config.get('module_dir'))
126
- end
138
+ pkg_path = if config.get('tests').first.include? 'windows'
139
+ CemAcpt::Utils.package_win_module(config.get('module_dir'))
140
+ else
141
+ CemAcpt::Utils::Puppet.build_module_package(config.get('module_dir'))
142
+ end
143
+ logger.info('CemAcpt::TestRunner') { "Created module package: #{pkg_path}..." }
144
+ pkg_path
127
145
  end
128
146
 
129
147
  # @return [Array<String>] The paths to the ssh private key, public key, and known hosts file
@@ -131,7 +149,9 @@ module CemAcpt
131
149
  return [nil, nil, nil] if config.get('no_ephemeral_ssh_key')
132
150
 
133
151
  logger.info('CemAcpt::TestRunner') { 'Creating ephemeral SSH keys...' }
134
- CemAcpt::Utils::SSH::Ephemeral.create
152
+ pri, pub, kh = CemAcpt::Utils::SSH::Ephemeral.create
153
+ logger.info('CemAcpt::TestRunner') { 'Created ephemeral SSH key pair...' }
154
+ [pri, pub, kh]
135
155
  end
136
156
 
137
157
  def clean_ephemeral_ssh_keys
@@ -143,12 +163,45 @@ module CemAcpt
143
163
 
144
164
  def new_test_data
145
165
  logger.debug('CemAcpt::TestRunner') { 'Creating new test data...' }
146
- CemAcpt::TestData.acceptance_test_data(config)
166
+ tdata = CemAcpt::TestData.acceptance_test_data(config)
167
+ logger.info('CemAcpt::TestRunner') { 'Created test data...' }
168
+ logger.verbose('CemAcpt::TestRunner') { "Test data:\n#{tdata}" }
169
+ tdata
147
170
  end
148
171
 
149
172
  def new_node_data
150
173
  logger.debug('CemAcpt::TestRunner') { 'Creating new node data...' }
151
- CemAcpt::Platform.use(config.get('platform.name'), config, @run_data)
174
+ ndata = CemAcpt::Platform.use(config.get('platform.name'), config, @run_data)
175
+ logger.info('CemAcpt::TestRunner') { 'Created node data...' }
176
+ logger.verbose('CemAcpt::TestRunner') { "Node data:\n#{ndata}" }
177
+ ndata
178
+ end
179
+
180
+ # Runs all methods that are needed to prep data and environment for the provision_test_nodes method
181
+ def pre_provision_test_nodes
182
+ logger.info('CemAcpt::TestRunner') { 'Pre-provisioning test nodes...' }
183
+ logger.info('CemAcpt::TestRunner') { 'Creating initial run data...' }
184
+ @run_data = {}
185
+ @run_data[:private_key], @run_data[:public_key], @run_data[:known_hosts] = new_ephemeral_ssh_keys
186
+ @run_data[:module_package_path] = build_module_package
187
+ @run_data[:test_data] = new_test_data
188
+ @run_data[:nodes] = new_node_data
189
+ logger.verbose('CemAcpt::TestRunner') { "Initial run data:\n#{@run_data}" }
190
+ logger.info('CemAcpt::TestRunner') { 'Created initial run data...' }
191
+ setup_bolt
192
+ end
193
+
194
+ def setup_bolt
195
+ logger.info('CemAcpt::TestRunner') { 'Setting up Bolt...' }
196
+ @bolt_test_runner = CemAcpt::Bolt::TestRunner.new(config, run_data: @run_data)
197
+ @bolt_test_runner.setup!
198
+ return unless @bolt_test_runner.tests.to_a.empty?
199
+
200
+ if !only_actions.empty? && only_actions.include?('bolt')
201
+ raise 'No Bolt tests to run and only bolt action was specified'
202
+ end
203
+
204
+ logger.warn('CemAcpt::TestRunner') { 'No Bolt tests to run' }
152
205
  end
153
206
 
154
207
  def provision_test_nodes
@@ -194,20 +247,60 @@ module CemAcpt
194
247
  logger.info('CemAcpt') { "Test SSH Keys:\n Private Key: #{@run_data[:private_key]}\n Public Key:#{@run_data[:public_key]}" }
195
248
  end
196
249
 
197
- def run_tests(hosts, only_actions, except_actions)
198
- logger.info('CemAcpt::TestRunner') { 'Running tests...' }
199
- logger.verbose('CemAcpt::TestRunner') { "Hosts: #{hosts}" }
200
- logger.verbose('CemAcpt::TestRunner') { "Only actions: #{only_actions}" }
201
- logger.verbose('CemAcpt::TestRunner') { "Except actions: #{except_actions}" }
202
- api_results = CemAcpt::Goss::Api.run_actions_async(hosts,
203
- only: only_actions || [],
204
- except: except_actions || [])
205
- res = []
206
- api_results.close unless api_results.closed?
207
- while (r = api_results.pop)
208
- res << ActionResult.new(r)
250
+ def run_tests
251
+ logger.info('CemAcpt::TestRunner') { 'Preparing to run tests...' }
252
+ logger.verbose('CemAcpt::TestRunner') { "Hosts: #{@hosts}" }
253
+ logger.verbose('CemAcpt::TestRunner') { "Only actions: #{CemAcpt::Actions.only}" }
254
+ logger.verbose('CemAcpt::TestRunner') { "Except actions: #{CemAcpt::Actions.except}" }
255
+ @results = CemAcpt::TestRunner::TestResults.new(config, @instance_names_ips)
256
+ CemAcpt::Actions.execute
257
+ end
258
+
259
+ def run_goss_tests(context = {})
260
+ logger.info('CemAcpt::TestRunner') { 'Running goss tests...' }
261
+ context[:hosts] = @hosts
262
+ context[:results] = @results
263
+ CemAcpt::Goss::Api.run_actions_async(context)
264
+ end
265
+
266
+ def run_bolt_tests(_context = {})
267
+ logger.info('CemAcpt::TestRunner') { 'Running Bolt tests...' }
268
+ # If the Bolt config has tests:only or tests:ignore lists, we need to filter the hosts
269
+ # based on their associated tests.
270
+ @bolt_test_runner.hosts = filtered_bolt_hosts
271
+ @bolt_test_runner.run
272
+ @results << @bolt_test_runner.results
273
+ end
274
+
275
+ def filtered_bolt_hosts
276
+ tests_only = config.get('bolt.tests.only')
277
+ tests_only_unset = tests_only.nil? || tests_only.empty?
278
+ logger.debug('CemAcpt::TestRunner') { "Bolt tests only: #{tests_only}" } unless tests_only_unset
279
+ tests_ignore = config.get('bolt.tests.ignore')
280
+ tests_ignore_unset = tests_ignore.nil? || tests_ignore.empty?
281
+ logger.debug('CemAcpt::TestRunner') { "Bolt tests ignore: #{tests_ignore}" } unless tests_ignore_unset
282
+ return @instance_names_ips.map { |_, v| v['ip'] } if tests_only_unset && tests_ignore_unset
283
+
284
+ logger.debug('CemAcpt::TestRunner') { 'Filtering Bolt hosts...' }
285
+ filtered_hosts = []
286
+ @instance_names_ips.each do |_, v|
287
+ host = v['ip']
288
+ test_name = v['test_name']
289
+ in_only = !tests_only_unset && tests_only.include?(test_name)
290
+ in_ignore = !tests_ignore_unset && tests_ignore.include?(test_name)
291
+ if in_only || !in_ignore
292
+ filtered_hosts << host
293
+ logger.debug('CemAcpt::TestRunner') { "Added host #{host} to filtered hosts" }
294
+ else
295
+ logger.debug('CemAcpt::TestRunner') do
296
+ "Not adding host #{host} to filtered hosts. In only? #{in_only}; In ignore? #{in_ignore}"
297
+ end
298
+ end
209
299
  end
210
- res
300
+ filtered_hosts.compact!
301
+ filtered_hosts.uniq!
302
+ logger.debug('CemAcpt::TestRunner') { "Filtered hosts: #{filtered_hosts}" }
303
+ filtered_hosts
211
304
  end
212
305
 
213
306
  def process_test_results
@@ -218,14 +311,14 @@ module CemAcpt
218
311
  logger.info('CemAcpt::TestRunner') { "Processing #{@results.size} test result(s)..." }
219
312
  until @results.empty?
220
313
  result = @results.pop
221
- @http_statuses << result.http_status
314
+ @statuses << result.status
222
315
  log_test_result(result)
223
316
  end
224
- if @http_statuses.empty?
317
+ if @statuses.empty?
225
318
  logger.error('CemAcpt::TestRunner') { 'No test results to process' }
226
319
  @exit_code = 1
227
320
  else
228
- @exit_code = (@http_statuses.any? { |s| s.to_i != 200 }) ? 1 : 0
321
+ @exit_code = (@statuses.any? { |s| SUCCESS_STATUS.include?(s.to_i) }) ? 1 : 0
229
322
  end
230
323
  end
231
324
  @duration = Time.now - @start_time
@@ -233,13 +326,22 @@ module CemAcpt
233
326
  end
234
327
 
235
328
  def log_test_result(result)
236
- result_log_formatter = LogFormatter.new_formatter(result, config, @instance_names_ips)
237
- logger.start_ci_group("Test results for #{result_log_formatter.test_name(result)}")
238
- return log_error_test_result(result_log_formatter, result) if result.error?
329
+ logger.start_ci_group("Test results for #{result.log_formatter.test_name}")
330
+ case result
331
+ when CemAcpt::TestRunner::TestResults::TestErrorActionResult
332
+ log_error_test_result(result)
333
+ when CemAcpt::TestRunner::TestResults::TestActionResult
334
+ log_action_test_result(result)
335
+ else
336
+ raise ArgumentError, "result must be a CemAcpt::TestRunner::TestResults::TestActionResult or CemAcpt::TestRunner::TestResults::TestErrorActionResult, got #{result.class}"
337
+ end
338
+ ensure
339
+ logger.end_ci_group
340
+ end
239
341
 
240
- logger.info(result_log_formatter.summary(result))
241
- formatted_results = result_log_formatter.results(result)
242
- formatted_results.each do |r|
342
+ def log_action_test_result(result)
343
+ logger.info { result.log_formatter.summary }
344
+ result.log_formatter.results.each do |r|
243
345
  if r.start_with?('Passed:')
244
346
  logger.verbose { r }
245
347
  elsif r.start_with?('Skipped:')
@@ -248,12 +350,10 @@ module CemAcpt
248
350
  logger.error { r }
249
351
  end
250
352
  end
251
- ensure
252
- logger.end_ci_group
253
353
  end
254
354
 
255
- def log_error_test_result(formatter, result)
256
- logger.fatal { formatter.results(result).join("\n") }
355
+ def log_error_test_result(result)
356
+ logger.fatal { result.log_formatter.results.join("\n") }
257
357
  end
258
358
 
259
359
  # Upload the cem_windows module to the bucket if we're testing the cem_windows module
@@ -0,0 +1,189 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require_relative '../logging'
5
+
6
+ module CemAcpt
7
+ module Utils
8
+ # Utility classes and methods for files
9
+ module Files
10
+ class << self
11
+ # Reads a file based on its extension
12
+ # @param file [String] Path to the file
13
+ # @param log_level [Symbol] Log level to use
14
+ # @param log_prefix [String] Log prefix to use
15
+ # @param kwargs [Hash] Keyword arguments to pass to the file utility
16
+ # @option kwargs [String] :log_msg Log message to use when logging the file operation
17
+ # @option kwargs [Array] :permitted_classes Array of classes to permit when loading YAML files
18
+ # @return [Object] The result of the file utility's read method
19
+ def read(file, *args, log_level: :debug, log_prefix: 'CemAcpt', **kwargs)
20
+ return from_content_registry(file, :content) unless file_changed?(file)
21
+
22
+ content = new_file_util_for(file, log_level: log_level, log_prefix: log_prefix).read(file, *args, **kwargs)
23
+ add_to_content_registry(file, :content, content)
24
+ content
25
+ end
26
+
27
+ def write(file, content, *args, log_level: :debug, log_prefix: 'CemAcpt', **kwargs)
28
+ new_file_util_for(file, log_level: log_level, log_prefix: log_prefix).write(file, content, *args, **kwargs)
29
+ end
30
+
31
+ def delete(file, *args, log_level: :debug, log_prefix: 'CemAcpt', **kwargs)
32
+ new_file_util_for(file, log_level: log_level, log_prefix: log_prefix).delete(file, *args, **kwargs)
33
+ end
34
+
35
+ private
36
+
37
+ def mutex
38
+ @mutex ||= Mutex.new
39
+ end
40
+
41
+ def content_registry
42
+ @content_registry ||= {}
43
+ end
44
+
45
+ def file_changed?(file)
46
+ return true unless File.exist?(file)
47
+ fstat = File.stat(file)
48
+ rmtime = from_content_registry(file, :mtime)
49
+ check_res = rmtime.nil? || (fstat.mtime != rmtime)
50
+ add_to_content_registry(file, :mtime, fstat.mtime)
51
+ check_res
52
+ end
53
+
54
+ def add_to_content_registry(file, property, value)
55
+ mutex.synchronize do
56
+ content_registry[file] ||= {}
57
+ content_registry[file][property.to_sym] = value
58
+ end
59
+ end
60
+
61
+ def from_content_registry(file, property)
62
+ mutex.synchronize do
63
+ content_registry[file] ||= {}
64
+ content_registry[file][property.to_sym]
65
+ end
66
+ end
67
+
68
+ def new_file_util_for(file, log_level: :debug, log_prefix: 'CemAcpt')
69
+ case File.extname(file)
70
+ when *YamlUtil::VALID_EXTS
71
+ YamlUtil.new(log_level: log_level, log_prefix: log_prefix)
72
+ when *JsonUtil::VALID_EXTS
73
+ JsonUtil.new(log_level: log_level, log_prefix: log_prefix)
74
+ else
75
+ FileUtil.new(log_level: log_level, log_prefix: log_prefix)
76
+ end
77
+ end
78
+ end
79
+
80
+ # Generic file utility class
81
+ class FileUtil
82
+ include CemAcpt::Logging
83
+
84
+ attr_reader :log_level, :log_prefix, :file_exts
85
+
86
+ def initialize(log_level: :debug, log_prefix: 'CemAcpt', file_exts: [])
87
+ @log_level = log_level
88
+ @log_prefix = log_prefix
89
+ @file_exts = file_exts
90
+ end
91
+
92
+ def log_level=(level)
93
+ level = level.downcase.to_sym
94
+ raise ArgumentError, "Invalid log level #{level}" unless logger.respond_to?(level)
95
+
96
+ @log_level = level
97
+ end
98
+
99
+ def log_prefix=(prefix)
100
+ @log_prefix = prefix.to_s
101
+ end
102
+
103
+ def file_exts=(exts)
104
+ raise ArgumentError, 'file_exts must be an Array' unless exts.is_a?(Array)
105
+
106
+ @file_ext = exts
107
+ end
108
+
109
+ def write(file, content, *_args, log_msg: 'Writing file %s...', **_kwargs)
110
+ validate_and_log(file, log_msg)
111
+ File.write(file, content)
112
+ end
113
+
114
+ def read(file, *_args, log_msg: 'Reading file %s...', **_kwargs)
115
+ validate_and_log(file, log_msg)
116
+ File.read(file)
117
+ end
118
+
119
+ def delete(file, *_args, log_msg: 'Deleting file %s...', **_kwargs)
120
+ validate_and_log(file, log_msg)
121
+ FileUtils.rm_f(file)
122
+ end
123
+
124
+ private
125
+
126
+ def validate_and_log(file, log_msg)
127
+ file = validate_ext(file)
128
+ logger.send(log_level, log_prefix) { log_msg.to_s % file }
129
+ end
130
+
131
+ def validate_ext(file)
132
+ return if file_exts.empty?
133
+
134
+ ext = File.extname(file)
135
+ raise ArgumentError, "Invalid file extension #{ext}! Valid file extensions are #{file_exts}" unless file_exts.include?(ext)
136
+ end
137
+ end
138
+
139
+ # Utility class for working with YAML files
140
+ class YamlUtil < FileUtil
141
+ VALID_EXTS = %w[.yaml .yml].freeze
142
+ DEFAULT_PERMITTED_CLASSES = [Symbol].freeze
143
+
144
+ def initialize(log_level: :debug, log_prefix: 'CemAcpt', file_exts: VALID_EXTS)
145
+ super(log_level: log_level, log_prefix: log_prefix, file_exts: file_exts)
146
+ require 'yaml'
147
+ end
148
+
149
+ def write(file, content, *_args, log_msg: 'Writing YAML file %s...', **_kwargs)
150
+ raise ArgumentError, 'content must be a Hash' unless content.is_a?(Hash)
151
+
152
+ super(file, content.to_yaml, log_msg)
153
+ end
154
+
155
+ def read(file, *_args, log_msg: 'Reading YAML file %s...', permitted_classes: DEFAULT_PERMITTED_CLASSES, **_kwargs)
156
+ YAML.safe_load(super(file, log_msg), permitted_classes: permitted_classes)
157
+ end
158
+
159
+ def delete(file, *_args, log_msg: 'Deleting YAML file %s...', **_kwargs)
160
+ super(file, log_msg)
161
+ end
162
+ end
163
+
164
+ # Utility class for working with JSON files
165
+ class JsonUtil < FileUtil
166
+ VALID_EXTS = %w[.json].freeze
167
+
168
+ def initialize(log_level: :debug, log_prefix: 'CemAcpt', file_exts: VALID_EXTS)
169
+ super(log_level: log_level, log_prefix: log_prefix, file_exts: file_exts)
170
+ require 'json'
171
+ end
172
+
173
+ def write(file, content, *_args, log_msg: 'Writing JSON file %s...', **_kwargs)
174
+ raise ArgumentError, 'content must be a Hash' unless content.is_a?(Hash)
175
+
176
+ super(file, content.to_json, log_msg)
177
+ end
178
+
179
+ def read(file, *_args, log_msg: 'Reading JSON file %s...', **_kwargs)
180
+ JSON.parse(super(file, log_msg))
181
+ end
182
+
183
+ def delete(file, *_args, log_msg: 'Deleting JSON file %s...', **_kwargs)
184
+ super(file, log_msg)
185
+ end
186
+ end
187
+ end
188
+ end
189
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CemAcpt
4
+ module Utils
5
+ class FinalizerQueueError < StandardError; end
6
+
7
+ # A queue that can be finalized.
8
+ # When a queue is finalized, no more items can be added to it, and the
9
+ # queue is closed and converted to a frozen array.
10
+ class FinalizerQueue
11
+ def initialize
12
+ @queue = Queue.new
13
+ @array = []
14
+ @finalized = false
15
+ @mutex = Mutex.new
16
+ end
17
+
18
+ def finalize!
19
+ return if finalized?
20
+
21
+ @finalized = true
22
+ new_array
23
+ end
24
+
25
+ def finalized?
26
+ @finalized
27
+ end
28
+
29
+ def to_a
30
+ raise FinalizerQueueError, 'Cannot convert to array until finalized' unless finalized?
31
+
32
+ @array
33
+ end
34
+
35
+ def method_missing(method_name, *args, **kwargs, &block)
36
+ if finalized?
37
+ @array.send(method_name, *args, **kwargs, &block)
38
+ elsif @queue.respond_to?(method_name)
39
+ @queue.send(method_name, *args, **kwargs, &block)
40
+ else
41
+ super
42
+ end
43
+ rescue StandardError => e
44
+ raise e if e.is_a?(NoMethodError) || e.is_a?(FinalizerQueueError)
45
+
46
+ new_err = FinalizerQueueError.new("Error calling #{method_name} on FinalizerQueue: #{e}")
47
+ new_err.set_backtrace(e.backtrace)
48
+ raise new_err
49
+ end
50
+
51
+ def respond_to_missing?(method_name, include_private = false)
52
+ @array.respond_to?(method_name, include_private) if finalized?
53
+
54
+ super
55
+ end
56
+
57
+ private
58
+
59
+ def new_array
60
+ @queue.close unless @queue.closed?
61
+ @array << @queue.pop until @queue.empty?
62
+ @array.compact!
63
+ @array.freeze
64
+ end
65
+
66
+ def require_finalized(caller_binding)
67
+ return if finalized?
68
+
69
+ raise FinalizerQueueError, "Cannot call #{caller_binding.eval('__method__')} on unfinalized #{self.class.name}"
70
+ end
71
+ end
72
+ end
73
+ end
@@ -6,6 +6,7 @@ require 'stringio'
6
6
  module CemAcpt
7
7
  # Error class for shell commands
8
8
  class ShellCommandError < StandardError; end
9
+ class ShellCommandNotFoundError < ShellCommandError; end
9
10
 
10
11
  module Utils
11
12
  # Generic utilities for running local shell commands
@@ -23,7 +24,7 @@ module CemAcpt
23
24
  io_outerr = StringIO.new
24
25
  if output.respond_to?(:debug)
25
26
  output.debug('CemAcpt::Utils::Shell') { "Running command:\n\t#{cmd}\nWith environment:\n\t#{env}" }
26
- else
27
+ elsif output
27
28
  output << "Running command:\n\t#{cmd}\nWith environment:\n\t#{env}\n"
28
29
  end
29
30
  val = Open3.popen2e(env, cmd) do |stdin, outerr, wait_thr|
@@ -47,18 +48,26 @@ module CemAcpt
47
48
 
48
49
  # Mimics the behavior of the `which` command.
49
50
  # @param cmd [String] The command to find
50
- # @return [String] The path to the command
51
- # @return [nil] If the command is not found
52
- def self.which(cmd)
51
+ # @param include_ruby_bin [Boolean] Whether to include Ruby bin directories in the search.
52
+ # Setting this to true can cause errors to be raised if cem_acpt attempts to use a Ruby
53
+ # command that is not available to cem_acpt, such as when running with `bundle exec`.
54
+ # @param raise_if_not_found [Boolean] Whether to raise an error if the command is not found
55
+ # @return [String, nil] The path to the command or nil if not found
56
+ # @raise [CemAcpt::ShellCommandNotFoundError] If the command is not found and raise_if_not_found is true
57
+ def self.which(cmd, include_ruby_bin: false, raise_if_not_found: false)
53
58
  return cmd if File.executable?(cmd) && !File.directory?(cmd)
54
59
 
55
60
  exts = ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') : ['']
56
61
  ENV['PATH'].split(File::PATH_SEPARATOR).each do |path|
62
+ next if path.include?('/ruby') && !include_ruby_bin
63
+
57
64
  exts.each do |ext|
58
65
  exe = File.join(path, "#{cmd}#{ext}")
59
66
  return exe if File.executable?(exe) && !File.directory?(exe)
60
67
  end
61
68
  end
69
+ raise CemAcpt::ShellCommandNotFoundError, "Command #{cmd} not found in PATH" if raise_if_not_found
70
+
62
71
  nil
63
72
  end
64
73
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module CemAcpt
4
- VERSION = '0.8.8'
4
+ VERSION = '0.9.1'
5
5
  end
data/sample_config.yaml CHANGED
@@ -58,6 +58,19 @@ tests:
58
58
  # - stig_rhel-7_firewalld_public_3
59
59
  # - stig_rhel-8_firewalld_public_3
60
60
 
61
+ bolt:
62
+ project:
63
+ name: 'cem-acpt'
64
+ analytics: false
65
+ tests:
66
+ only: [] # Test names from the "tests" array above
67
+ ignore: []
68
+ tasks:
69
+ ignore: [] # Task names to ignore
70
+ only: []
71
+ module_pattern: '^.*$'
72
+ name_filter: '^fake_task$'
73
+
61
74
  cem_acpt_image:
62
75
  no_windows: true
63
76
  no_linux: false