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
         |