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.
- checksums.yaml +4 -4
- data/.github/workflows/spec.yml +0 -3
- data/Gemfile.lock +9 -1
- data/README.md +95 -13
- data/cem_acpt.gemspec +2 -1
- data/lib/cem_acpt/action_result.rb +8 -2
- data/lib/cem_acpt/actions.rb +153 -0
- data/lib/cem_acpt/bolt/cmd/base.rb +174 -0
- data/lib/cem_acpt/bolt/cmd/output.rb +315 -0
- data/lib/cem_acpt/bolt/cmd/task.rb +59 -0
- data/lib/cem_acpt/bolt/cmd.rb +22 -0
- data/lib/cem_acpt/bolt/errors.rb +49 -0
- data/lib/cem_acpt/bolt/helpers.rb +52 -0
- data/lib/cem_acpt/bolt/inventory.rb +62 -0
- data/lib/cem_acpt/bolt/project.rb +38 -0
- data/lib/cem_acpt/bolt/summary_results.rb +96 -0
- data/lib/cem_acpt/bolt/tasks.rb +181 -0
- data/lib/cem_acpt/bolt/tests.rb +415 -0
- data/lib/cem_acpt/bolt/yaml_file.rb +74 -0
- data/lib/cem_acpt/bolt.rb +142 -0
- data/lib/cem_acpt/cli.rb +6 -0
- data/lib/cem_acpt/config/base.rb +4 -0
- data/lib/cem_acpt/config/cem_acpt.rb +7 -1
- data/lib/cem_acpt/core_ext.rb +25 -0
- data/lib/cem_acpt/goss/api/action_response.rb +4 -0
- data/lib/cem_acpt/goss/api.rb +23 -25
- data/lib/cem_acpt/image_builder/provision_commands.rb +43 -0
- data/lib/cem_acpt/logging/formatter.rb +3 -3
- data/lib/cem_acpt/logging.rb +17 -1
- data/lib/cem_acpt/provision/terraform/linux.rb +2 -2
- data/lib/cem_acpt/test_data.rb +2 -0
- data/lib/cem_acpt/test_runner/log_formatter/base.rb +73 -0
- data/lib/cem_acpt/test_runner/log_formatter/bolt_error_formatter.rb +65 -0
- data/lib/cem_acpt/test_runner/log_formatter/bolt_output_formatter.rb +54 -0
- data/lib/cem_acpt/test_runner/log_formatter/bolt_summary_results_formatter.rb +64 -0
- data/lib/cem_acpt/test_runner/log_formatter/goss_action_response.rb +17 -30
- data/lib/cem_acpt/test_runner/log_formatter/goss_error_formatter.rb +31 -0
- data/lib/cem_acpt/test_runner/log_formatter/standard_error_formatter.rb +35 -0
- data/lib/cem_acpt/test_runner/log_formatter.rb +17 -5
- data/lib/cem_acpt/test_runner/test_results.rb +150 -0
- data/lib/cem_acpt/test_runner.rb +153 -53
- data/lib/cem_acpt/utils/files.rb +189 -0
- data/lib/cem_acpt/utils/finalizer_queue.rb +73 -0
- data/lib/cem_acpt/utils/shell.rb +13 -4
- data/lib/cem_acpt/version.rb +1 -1
- data/sample_config.yaml +13 -0
- metadata +41 -5
- data/lib/cem_acpt/test_runner/log_formatter/error_formatter.rb +0 -33
data/lib/cem_acpt/test_runner.rb
CHANGED
@@ -2,7 +2,8 @@
|
|
2
2
|
|
3
3
|
require 'fileutils'
|
4
4
|
require 'securerandom'
|
5
|
-
require_relative '
|
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
|
-
@
|
31
|
-
@
|
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
|
-
|
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
|
-
@
|
83
|
-
|
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 <<
|
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
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
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
|
198
|
-
logger.info('CemAcpt::TestRunner') { '
|
199
|
-
logger.verbose('CemAcpt::TestRunner') { "Hosts: #{hosts}" }
|
200
|
-
logger.verbose('CemAcpt::TestRunner') { "Only actions: #{
|
201
|
-
logger.verbose('CemAcpt::TestRunner') { "Except actions: #{
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
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
|
-
|
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
|
-
@
|
314
|
+
@statuses << result.status
|
222
315
|
log_test_result(result)
|
223
316
|
end
|
224
|
-
if @
|
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 = (@
|
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
|
-
|
237
|
-
|
238
|
-
|
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
|
-
|
241
|
-
|
242
|
-
|
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(
|
256
|
-
logger.fatal {
|
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
|
data/lib/cem_acpt/utils/shell.rb
CHANGED
@@ -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
|
-
|
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
|
-
# @
|
51
|
-
#
|
52
|
-
|
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
|
data/lib/cem_acpt/version.rb
CHANGED
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
|