cem_acpt 0.8.8 → 0.9.1

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 (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